2 * Copyright (C) 2016 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.server.display;
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.TypeEvaluator;
22 import android.animation.ValueAnimator;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.app.AlarmManager;
26 import android.content.BroadcastReceiver;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.database.ContentObserver;
32 import android.net.Uri;
33 import android.opengl.Matrix;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.UserHandle;
37 import android.provider.Settings.Secure;
38 import android.util.MathUtils;
39 import android.util.Slog;
40 import android.view.animation.AnimationUtils;
42 import com.android.internal.app.NightDisplayController;
43 import com.android.server.SystemService;
44 import com.android.server.twilight.TwilightListener;
45 import com.android.server.twilight.TwilightManager;
46 import com.android.server.twilight.TwilightState;
48 import java.util.Calendar;
49 import java.util.TimeZone;
51 import static com.android.server.display.DisplayTransformManager.LEVEL_COLOR_MATRIX_NIGHT_DISPLAY;
54 * Tints the display at night.
56 public final class NightDisplayService extends SystemService
57 implements NightDisplayController.Callback {
59 private static final String TAG = "NightDisplayService";
60 private static final boolean DEBUG = false;
63 * Night display ~= 3400 K.
65 private static final float[] MATRIX_NIGHT = new float[] {
73 * The identity matrix, used if one of the given matrices is {@code null}.
75 private static final float[] MATRIX_IDENTITY = new float[16];
77 Matrix.setIdentityM(MATRIX_IDENTITY, 0);
81 * Evaluator used to animate color matrix transitions.
83 private static final ColorMatrixEvaluator COLOR_MATRIX_EVALUATOR = new ColorMatrixEvaluator();
85 private final Handler mHandler;
87 private int mCurrentUser = UserHandle.USER_NULL;
88 private ContentObserver mUserSetupObserver;
89 private boolean mBootCompleted;
91 private NightDisplayController mController;
92 private ValueAnimator mColorMatrixAnimator;
93 private Boolean mIsActivated;
94 private AutoMode mAutoMode;
96 public NightDisplayService(Context context) {
98 mHandler = new Handler(Looper.getMainLooper());
102 public void onStart() {
103 // Nothing to publish.
107 public void onBootPhase(int phase) {
108 if (phase == PHASE_BOOT_COMPLETED) {
109 mBootCompleted = true;
111 // Register listeners now that boot is complete.
112 if (mCurrentUser != UserHandle.USER_NULL && mUserSetupObserver == null) {
119 public void onStartUser(int userHandle) {
120 super.onStartUser(userHandle);
122 if (mCurrentUser == UserHandle.USER_NULL) {
123 onUserChanged(userHandle);
128 public void onSwitchUser(int userHandle) {
129 super.onSwitchUser(userHandle);
131 onUserChanged(userHandle);
135 public void onStopUser(int userHandle) {
136 super.onStopUser(userHandle);
138 if (mCurrentUser == userHandle) {
139 onUserChanged(UserHandle.USER_NULL);
143 private void onUserChanged(int userHandle) {
144 final ContentResolver cr = getContext().getContentResolver();
146 if (mCurrentUser != UserHandle.USER_NULL) {
147 if (mUserSetupObserver != null) {
148 cr.unregisterContentObserver(mUserSetupObserver);
149 mUserSetupObserver = null;
150 } else if (mBootCompleted) {
155 mCurrentUser = userHandle;
157 if (mCurrentUser != UserHandle.USER_NULL) {
158 if (!isUserSetupCompleted(cr, mCurrentUser)) {
159 mUserSetupObserver = new ContentObserver(mHandler) {
161 public void onChange(boolean selfChange, Uri uri) {
162 if (isUserSetupCompleted(cr, mCurrentUser)) {
163 cr.unregisterContentObserver(this);
164 mUserSetupObserver = null;
166 if (mBootCompleted) {
172 cr.registerContentObserver(Secure.getUriFor(Secure.USER_SETUP_COMPLETE),
173 false /* notifyForDescendents */, mUserSetupObserver, mCurrentUser);
174 } else if (mBootCompleted) {
180 private static boolean isUserSetupCompleted(ContentResolver cr, int userHandle) {
181 return Secure.getIntForUser(cr, Secure.USER_SETUP_COMPLETE, 0, userHandle) == 1;
184 private void setUp() {
185 // Create a new controller for the current user and start listening for changes.
186 mController = new NightDisplayController(getContext(), mCurrentUser);
187 mController.setListener(this);
189 // Initialize the current auto mode.
190 onAutoModeChanged(mController.getAutoMode());
192 // Force the initialization current activated state.
193 if (mIsActivated == null) {
194 onActivated(mController.isActivated());
198 private void tearDown() {
199 if (mController != null) {
200 mController.setListener(null);
204 if (mAutoMode != null) {
209 if (mColorMatrixAnimator != null) {
210 mColorMatrixAnimator.end();
211 mColorMatrixAnimator = null;
218 public void onActivated(boolean activated) {
219 if (mIsActivated == null || mIsActivated != activated) {
220 Slog.i(TAG, activated ? "Turning on night display" : "Turning off night display");
222 if (mAutoMode != null) {
223 mAutoMode.onActivated(activated);
226 mIsActivated = activated;
228 // Cancel the old animator if still running.
229 if (mColorMatrixAnimator != null) {
230 mColorMatrixAnimator.cancel();
233 final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class);
234 final float[] from = dtm.getColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY);
235 final float[] to = mIsActivated ? MATRIX_NIGHT : null;
237 mColorMatrixAnimator = ValueAnimator.ofObject(COLOR_MATRIX_EVALUATOR,
238 from == null ? MATRIX_IDENTITY : from, to == null ? MATRIX_IDENTITY : to);
239 mColorMatrixAnimator.setDuration(getContext().getResources()
240 .getInteger(android.R.integer.config_longAnimTime));
241 mColorMatrixAnimator.setInterpolator(AnimationUtils.loadInterpolator(
242 getContext(), android.R.interpolator.fast_out_slow_in));
243 mColorMatrixAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
245 public void onAnimationUpdate(ValueAnimator animator) {
246 final float[] value = (float[]) animator.getAnimatedValue();
247 dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, value);
250 mColorMatrixAnimator.addListener(new AnimatorListenerAdapter() {
252 private boolean mIsCancelled;
255 public void onAnimationCancel(Animator animator) {
260 public void onAnimationEnd(Animator animator) {
262 // Ensure final color matrix is set at the end of the animation. If the
263 // animation is cancelled then don't set the final color matrix so the new
264 // animator can pick up from where this one left off.
265 dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, to);
267 mColorMatrixAnimator = null;
270 mColorMatrixAnimator.start();
275 public void onAutoModeChanged(int autoMode) {
276 if (mAutoMode != null) {
281 if (autoMode == NightDisplayController.AUTO_MODE_CUSTOM) {
282 mAutoMode = new CustomAutoMode();
283 } else if (autoMode == NightDisplayController.AUTO_MODE_TWILIGHT) {
284 mAutoMode = new TwilightAutoMode();
287 if (mAutoMode != null) {
293 public void onCustomStartTimeChanged(NightDisplayController.LocalTime startTime) {
294 if (mAutoMode != null) {
295 mAutoMode.onCustomStartTimeChanged(startTime);
300 public void onCustomEndTimeChanged(NightDisplayController.LocalTime endTime) {
301 if (mAutoMode != null) {
302 mAutoMode.onCustomEndTimeChanged(endTime);
306 private abstract class AutoMode implements NightDisplayController.Callback {
307 public abstract void onStart();
308 public abstract void onStop();
311 private class CustomAutoMode extends AutoMode implements AlarmManager.OnAlarmListener {
313 private final AlarmManager mAlarmManager;
314 private final BroadcastReceiver mTimeChangedReceiver;
316 private NightDisplayController.LocalTime mStartTime;
317 private NightDisplayController.LocalTime mEndTime;
319 private Calendar mLastActivatedTime;
321 public CustomAutoMode() {
322 mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
323 mTimeChangedReceiver = new BroadcastReceiver() {
325 public void onReceive(Context context, Intent intent) {
331 private void updateActivated() {
332 final Calendar now = Calendar.getInstance();
333 final Calendar startTime = mStartTime.getDateTimeBefore(now);
334 final Calendar endTime = mEndTime.getDateTimeAfter(startTime);
335 final boolean activated = now.before(endTime);
337 boolean setActivated = mIsActivated == null || mLastActivatedTime == null;
338 if (!setActivated && mIsActivated != activated) {
339 final TimeZone currentTimeZone = now.getTimeZone();
340 if (!currentTimeZone.equals(mLastActivatedTime.getTimeZone())) {
341 final int year = mLastActivatedTime.get(Calendar.YEAR);
342 final int dayOfYear = mLastActivatedTime.get(Calendar.DAY_OF_YEAR);
343 final int hourOfDay = mLastActivatedTime.get(Calendar.HOUR_OF_DAY);
344 final int minute = mLastActivatedTime.get(Calendar.MINUTE);
346 mLastActivatedTime.setTimeZone(currentTimeZone);
347 mLastActivatedTime.set(Calendar.YEAR, year);
348 mLastActivatedTime.set(Calendar.DAY_OF_YEAR, dayOfYear);
349 mLastActivatedTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
350 mLastActivatedTime.set(Calendar.MINUTE, minute);
354 setActivated = now.before(mStartTime.getDateTimeBefore(mLastActivatedTime))
355 || now.after(mEndTime.getDateTimeAfter(mLastActivatedTime));
357 setActivated = now.before(mEndTime.getDateTimeBefore(mLastActivatedTime))
358 || now.after(mStartTime.getDateTimeAfter(mLastActivatedTime));
363 mController.setActivated(activated);
365 updateNextAlarm(mIsActivated, now);
368 private void updateNextAlarm(@Nullable Boolean activated, @NonNull Calendar now) {
369 if (activated != null) {
370 final Calendar next = activated ? mEndTime.getDateTimeAfter(now)
371 : mStartTime.getDateTimeAfter(now);
372 mAlarmManager.setExact(AlarmManager.RTC, next.getTimeInMillis(), TAG, this, null);
377 public void onStart() {
378 final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
379 intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
380 getContext().registerReceiver(mTimeChangedReceiver, intentFilter);
382 mStartTime = mController.getCustomStartTime();
383 mEndTime = mController.getCustomEndTime();
385 // Force an update to initialize state.
390 public void onStop() {
391 getContext().unregisterReceiver(mTimeChangedReceiver);
393 mAlarmManager.cancel(this);
394 mLastActivatedTime = null;
398 public void onActivated(boolean activated) {
399 final Calendar now = Calendar.getInstance();
400 if (mIsActivated != null) {
401 mLastActivatedTime = now;
403 updateNextAlarm(activated, now);
407 public void onCustomStartTimeChanged(NightDisplayController.LocalTime startTime) {
408 mStartTime = startTime;
409 mLastActivatedTime = null;
414 public void onCustomEndTimeChanged(NightDisplayController.LocalTime endTime) {
416 mLastActivatedTime = null;
421 public void onAlarm() {
422 if (DEBUG) Slog.d(TAG, "onAlarm");
427 private class TwilightAutoMode extends AutoMode implements TwilightListener {
429 private final TwilightManager mTwilightManager;
431 private Calendar mLastActivatedTime;
433 public TwilightAutoMode() {
434 mTwilightManager = getLocalService(TwilightManager.class);
437 private void updateActivated(TwilightState state) {
438 final boolean isNight = state != null && state.isNight();
439 boolean setActivated = mIsActivated == null || mIsActivated != isNight;
440 if (setActivated && state != null && mLastActivatedTime != null) {
441 final Calendar sunrise = state.sunrise();
442 final Calendar sunset = state.sunset();
443 if (sunrise.before(sunset)) {
444 setActivated = mLastActivatedTime.before(sunrise)
445 || mLastActivatedTime.after(sunset);
447 setActivated = mLastActivatedTime.before(sunset)
448 || mLastActivatedTime.after(sunrise);
453 mController.setActivated(isNight);
458 public void onStart() {
459 mTwilightManager.registerListener(this, mHandler);
461 // Force an update to initialize state.
462 updateActivated(mTwilightManager.getLastTwilightState());
466 public void onStop() {
467 mTwilightManager.unregisterListener(this);
468 mLastActivatedTime = null;
472 public void onActivated(boolean activated) {
473 if (mIsActivated != null) {
474 mLastActivatedTime = Calendar.getInstance();
479 public void onTwilightStateChanged(@Nullable TwilightState state) {
480 if (DEBUG) Slog.d(TAG, "onTwilightStateChanged");
481 updateActivated(state);
486 * Interpolates between two 4x4 color transform matrices (in column-major order).
488 private static class ColorMatrixEvaluator implements TypeEvaluator<float[]> {
491 * Result matrix returned by {@link #evaluate(float, float[], float[])}.
493 private final float[] mResultMatrix = new float[16];
496 public float[] evaluate(float fraction, float[] startValue, float[] endValue) {
497 for (int i = 0; i < mResultMatrix.length; i++) {
498 mResultMatrix[i] = MathUtils.lerp(startValue[i], endValue[i], fraction);
500 return mResultMatrix;