1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package com.android.server.display;
18 
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.RemoteException;
37 import android.os.UserHandle;
38 import android.provider.Settings.Secure;
39 import android.service.vr.IVrManager;
40 import android.service.vr.IVrStateCallbacks;
41 import android.util.MathUtils;
42 import android.util.Slog;
43 import android.view.animation.AnimationUtils;
44 
45 import com.android.internal.app.NightDisplayController;
46 import com.android.server.SystemService;
47 import com.android.server.twilight.TwilightListener;
48 import com.android.server.twilight.TwilightManager;
49 import com.android.server.twilight.TwilightState;
50 
51 import java.util.concurrent.atomic.AtomicBoolean;
52 import java.util.Calendar;
53 import java.util.TimeZone;
54 
55 import static com.android.server.display.DisplayTransformManager.LEVEL_COLOR_MATRIX_NIGHT_DISPLAY;
56 
57 /**
58  * Tints the display at night.
59  */
60 public final class NightDisplayService extends SystemService
61         implements NightDisplayController.Callback {
62 
63     private static final String TAG = "NightDisplayService";
64 
65     /**
66      * The transition time, in milliseconds, for Night Display to turn on/off.
67      */
68     private static final long TRANSITION_DURATION = 3000L;
69 
70     /**
71      * The identity matrix, used if one of the given matrices is {@code null}.
72      */
73     private static final float[] MATRIX_IDENTITY = new float[16];
74     static {
Matrix.setIdentityM(MATRIX_IDENTITY, 0)75         Matrix.setIdentityM(MATRIX_IDENTITY, 0);
76     }
77 
78     /**
79      * Evaluator used to animate color matrix transitions.
80      */
81     private static final ColorMatrixEvaluator COLOR_MATRIX_EVALUATOR = new ColorMatrixEvaluator();
82 
83     private final Handler mHandler;
84     private final AtomicBoolean mIgnoreAllColorMatrixChanges = new AtomicBoolean();
85     private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
86         @Override
87         public void onVrStateChanged(final boolean enabled) {
88             // Turn off all night mode display stuff while device is in VR mode.
89             mIgnoreAllColorMatrixChanges.set(enabled);
90             mHandler.post(new Runnable() {
91                 @Override
92                 public void run() {
93                     // Cancel in-progress animations
94                     if (mColorMatrixAnimator != null) {
95                         mColorMatrixAnimator.cancel();
96                     }
97 
98                     final DisplayTransformManager dtm =
99                             getLocalService(DisplayTransformManager.class);
100                     if (enabled) {
101                         dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, MATRIX_IDENTITY);
102                     } else if (mController != null && mController.isActivated()) {
103                         setMatrix(mController.getColorTemperature(), mMatrixNight);
104                         dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, mMatrixNight);
105                     }
106                 }
107             });
108         }
109     };
110 
111     private float[] mMatrixNight = new float[16];
112 
113     /**
114      *  These coefficients were generated by an LLS quadratic regression fitted to the
115      *  overdetermined system based on experimental readings (and subsequent conversion from xy
116      *  chromaticity coordinates to gamma-corrected RGB values): { (temperature, R, G, B) } ->
117      *  { (7304, 1.0, 1.0, 1.0), (4082, 1.0, 0.857, 0.719), (2850, 1.0, .754, .516),
118      *  (2596, 1.0, 0.722, 0.454) }. The 3x3 matrix is formatted like so:
119      *  <table>
120      *      <tr><td>R: a coefficient</td><td>G: a coefficient</td><td>B: a coefficient</td></tr>
121      *      <tr><td>R: b coefficient</td><td>G: b coefficient</td><td>B: b coefficient</td></tr>
122      *      <tr><td>R: y-intercept</td><td>G: y-intercept</td><td>B: y-intercept</td></tr>
123      *  </table>
124      */
125     private static final float[] mColorTempCoefficients = new float[] {
126             0.0f, -0.00000000962353339f, -0.0000000189359041f,
127             0.0f, 0.000153045476f, 0.000302412211f,
128             1.0f, 0.390782778f, -0.198650895f
129     };
130 
131     private int mCurrentUser = UserHandle.USER_NULL;
132     private ContentObserver mUserSetupObserver;
133     private boolean mBootCompleted;
134 
135     private NightDisplayController mController;
136     private ValueAnimator mColorMatrixAnimator;
137     private Boolean mIsActivated;
138     private AutoMode mAutoMode;
139 
NightDisplayService(Context context)140     public NightDisplayService(Context context) {
141         super(context);
142         mHandler = new Handler(Looper.getMainLooper());
143     }
144 
145     @Override
onStart()146     public void onStart() {
147         // Nothing to publish.
148     }
149 
150     @Override
onBootPhase(int phase)151     public void onBootPhase(int phase) {
152         if (phase >= PHASE_SYSTEM_SERVICES_READY) {
153             final IVrManager vrManager = IVrManager.Stub.asInterface(
154                     getBinderService(Context.VR_SERVICE));
155             if (vrManager != null) {
156                 try {
157                     vrManager.registerListener(mVrStateCallbacks);
158                 } catch (RemoteException e) {
159                     Slog.e(TAG, "Failed to register VR mode state listener: " + e);
160                 }
161             }
162         }
163 
164         if (phase >= PHASE_BOOT_COMPLETED) {
165             mBootCompleted = true;
166 
167             // Register listeners now that boot is complete.
168             if (mCurrentUser != UserHandle.USER_NULL && mUserSetupObserver == null) {
169                 setUp();
170             }
171         }
172     }
173 
174     @Override
onStartUser(int userHandle)175     public void onStartUser(int userHandle) {
176         super.onStartUser(userHandle);
177 
178         if (mCurrentUser == UserHandle.USER_NULL) {
179             onUserChanged(userHandle);
180         }
181     }
182 
183     @Override
onSwitchUser(int userHandle)184     public void onSwitchUser(int userHandle) {
185         super.onSwitchUser(userHandle);
186 
187         onUserChanged(userHandle);
188     }
189 
190     @Override
onStopUser(int userHandle)191     public void onStopUser(int userHandle) {
192         super.onStopUser(userHandle);
193 
194         if (mCurrentUser == userHandle) {
195             onUserChanged(UserHandle.USER_NULL);
196         }
197     }
198 
onUserChanged(int userHandle)199     private void onUserChanged(int userHandle) {
200         final ContentResolver cr = getContext().getContentResolver();
201 
202         if (mCurrentUser != UserHandle.USER_NULL) {
203             if (mUserSetupObserver != null) {
204                 cr.unregisterContentObserver(mUserSetupObserver);
205                 mUserSetupObserver = null;
206             } else if (mBootCompleted) {
207                 tearDown();
208             }
209         }
210 
211         mCurrentUser = userHandle;
212 
213         if (mCurrentUser != UserHandle.USER_NULL) {
214             if (!isUserSetupCompleted(cr, mCurrentUser)) {
215                 mUserSetupObserver = new ContentObserver(mHandler) {
216                     @Override
217                     public void onChange(boolean selfChange, Uri uri) {
218                         if (isUserSetupCompleted(cr, mCurrentUser)) {
219                             cr.unregisterContentObserver(this);
220                             mUserSetupObserver = null;
221 
222                             if (mBootCompleted) {
223                                 setUp();
224                             }
225                         }
226                     }
227                 };
228                 cr.registerContentObserver(Secure.getUriFor(Secure.USER_SETUP_COMPLETE),
229                         false /* notifyForDescendents */, mUserSetupObserver, mCurrentUser);
230             } else if (mBootCompleted) {
231                 setUp();
232             }
233         }
234     }
235 
isUserSetupCompleted(ContentResolver cr, int userHandle)236     private static boolean isUserSetupCompleted(ContentResolver cr, int userHandle) {
237         return Secure.getIntForUser(cr, Secure.USER_SETUP_COMPLETE, 0, userHandle) == 1;
238     }
239 
setUp()240     private void setUp() {
241         Slog.d(TAG, "setUp: currentUser=" + mCurrentUser);
242 
243         // Create a new controller for the current user and start listening for changes.
244         mController = new NightDisplayController(getContext(), mCurrentUser);
245         mController.setListener(this);
246 
247         // Prepare color transformation matrix.
248         setMatrix(mController.getColorTemperature(), mMatrixNight);
249 
250         // Initialize the current auto mode.
251         onAutoModeChanged(mController.getAutoMode());
252 
253         // Force the initialization current activated state.
254         if (mIsActivated == null) {
255             onActivated(mController.isActivated());
256         }
257 
258         // Transition the screen to the current temperature.
259         applyTint(false);
260     }
261 
tearDown()262     private void tearDown() {
263         Slog.d(TAG, "tearDown: currentUser=" + mCurrentUser);
264 
265         if (mController != null) {
266             mController.setListener(null);
267             mController = null;
268         }
269 
270         if (mAutoMode != null) {
271             mAutoMode.onStop();
272             mAutoMode = null;
273         }
274 
275         if (mColorMatrixAnimator != null) {
276             mColorMatrixAnimator.end();
277             mColorMatrixAnimator = null;
278         }
279 
280         mIsActivated = null;
281     }
282 
283     @Override
onActivated(boolean activated)284     public void onActivated(boolean activated) {
285         if (mIsActivated == null || mIsActivated != activated) {
286             Slog.i(TAG, activated ? "Turning on night display" : "Turning off night display");
287 
288             mIsActivated = activated;
289 
290             if (mAutoMode != null) {
291                 mAutoMode.onActivated(activated);
292             }
293 
294             applyTint(false);
295         }
296     }
297 
298     @Override
onAutoModeChanged(int autoMode)299     public void onAutoModeChanged(int autoMode) {
300         Slog.d(TAG, "onAutoModeChanged: autoMode=" + autoMode);
301 
302         if (mAutoMode != null) {
303             mAutoMode.onStop();
304             mAutoMode = null;
305         }
306 
307         if (autoMode == NightDisplayController.AUTO_MODE_CUSTOM) {
308             mAutoMode = new CustomAutoMode();
309         } else if (autoMode == NightDisplayController.AUTO_MODE_TWILIGHT) {
310             mAutoMode = new TwilightAutoMode();
311         }
312 
313         if (mAutoMode != null) {
314             mAutoMode.onStart();
315         }
316     }
317 
318     @Override
onCustomStartTimeChanged(NightDisplayController.LocalTime startTime)319     public void onCustomStartTimeChanged(NightDisplayController.LocalTime startTime) {
320         Slog.d(TAG, "onCustomStartTimeChanged: startTime=" + startTime);
321 
322         if (mAutoMode != null) {
323             mAutoMode.onCustomStartTimeChanged(startTime);
324         }
325     }
326 
327     @Override
onCustomEndTimeChanged(NightDisplayController.LocalTime endTime)328     public void onCustomEndTimeChanged(NightDisplayController.LocalTime endTime) {
329         Slog.d(TAG, "onCustomEndTimeChanged: endTime=" + endTime);
330 
331         if (mAutoMode != null) {
332             mAutoMode.onCustomEndTimeChanged(endTime);
333         }
334     }
335 
336     @Override
onColorTemperatureChanged(int colorTemperature)337     public void onColorTemperatureChanged(int colorTemperature) {
338         setMatrix(colorTemperature, mMatrixNight);
339         applyTint(true);
340     }
341 
342     /**
343      * Applies current color temperature matrix, or removes it if deactivated.
344      *
345      * @param immediate {@code true} skips transition animation
346      */
applyTint(boolean immediate)347     private void applyTint(boolean immediate) {
348         // Cancel the old animator if still running.
349         if (mColorMatrixAnimator != null) {
350             mColorMatrixAnimator.cancel();
351         }
352 
353         // Don't do any color matrix change animations if we are ignoring them anyway.
354         if (mIgnoreAllColorMatrixChanges.get()) {
355             return;
356         }
357 
358         final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class);
359         final float[] from = dtm.getColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY);
360         final float[] to = mIsActivated ? mMatrixNight : MATRIX_IDENTITY;
361 
362         if (immediate) {
363             dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, to);
364         } else {
365             mColorMatrixAnimator = ValueAnimator.ofObject(COLOR_MATRIX_EVALUATOR,
366                     from == null ? MATRIX_IDENTITY : from, to);
367             mColorMatrixAnimator.setDuration(TRANSITION_DURATION);
368             mColorMatrixAnimator.setInterpolator(AnimationUtils.loadInterpolator(
369                     getContext(), android.R.interpolator.fast_out_slow_in));
370             mColorMatrixAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
371                 @Override
372                 public void onAnimationUpdate(ValueAnimator animator) {
373                     final float[] value = (float[]) animator.getAnimatedValue();
374                     dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, value);
375                 }
376             });
377             mColorMatrixAnimator.addListener(new AnimatorListenerAdapter() {
378 
379                 private boolean mIsCancelled;
380 
381                 @Override
382                 public void onAnimationCancel(Animator animator) {
383                     mIsCancelled = true;
384                 }
385 
386                 @Override
387                 public void onAnimationEnd(Animator animator) {
388                     if (!mIsCancelled) {
389                         // Ensure final color matrix is set at the end of the animation. If the
390                         // animation is cancelled then don't set the final color matrix so the new
391                         // animator can pick up from where this one left off.
392                         dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, to);
393                     }
394                     mColorMatrixAnimator = null;
395                 }
396             });
397             mColorMatrixAnimator.start();
398         }
399     }
400 
401     /**
402      * Set the color transformation {@code MATRIX_NIGHT} to the given color temperature.
403      *
404      * @param colorTemperature color temperature in Kelvin
405      * @param outTemp          the 4x4 display transformation matrix for that color temperature
406      */
setMatrix(int colorTemperature, float[] outTemp)407     private void setMatrix(int colorTemperature, float[] outTemp) {
408         if (outTemp.length != 16) {
409             Slog.d(TAG, "The display transformation matrix must be 4x4");
410             return;
411         }
412 
413         Matrix.setIdentityM(mMatrixNight, 0);
414 
415         final float squareTemperature = colorTemperature * colorTemperature;
416         final float red = squareTemperature * mColorTempCoefficients[0]
417                 + colorTemperature * mColorTempCoefficients[3] + mColorTempCoefficients[6];
418         final float green = squareTemperature * mColorTempCoefficients[1]
419                 + colorTemperature * mColorTempCoefficients[4] + mColorTempCoefficients[7];
420         final float blue = squareTemperature * mColorTempCoefficients[2]
421                 + colorTemperature * mColorTempCoefficients[5] + mColorTempCoefficients[8];
422         outTemp[0] = red;
423         outTemp[5] = green;
424         outTemp[10] = blue;
425     }
426 
427     private abstract class AutoMode implements NightDisplayController.Callback {
onStart()428         public abstract void onStart();
429 
onStop()430         public abstract void onStop();
431     }
432 
433     private class CustomAutoMode extends AutoMode implements AlarmManager.OnAlarmListener {
434 
435         private final AlarmManager mAlarmManager;
436         private final BroadcastReceiver mTimeChangedReceiver;
437 
438         private NightDisplayController.LocalTime mStartTime;
439         private NightDisplayController.LocalTime mEndTime;
440 
441         private Calendar mLastActivatedTime;
442 
CustomAutoMode()443         CustomAutoMode() {
444             mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
445             mTimeChangedReceiver = new BroadcastReceiver() {
446                 @Override
447                 public void onReceive(Context context, Intent intent) {
448                     updateActivated();
449                 }
450             };
451         }
452 
updateActivated()453         private void updateActivated() {
454             final Calendar now = Calendar.getInstance();
455             final Calendar startTime = mStartTime.getDateTimeBefore(now);
456             final Calendar endTime = mEndTime.getDateTimeAfter(startTime);
457 
458             boolean activate = now.before(endTime);
459             if (mLastActivatedTime != null) {
460                 // Convert mLastActivatedTime to the current timezone if needed.
461                 final TimeZone currentTimeZone = now.getTimeZone();
462                 if (!currentTimeZone.equals(mLastActivatedTime.getTimeZone())) {
463                     final int year = mLastActivatedTime.get(Calendar.YEAR);
464                     final int dayOfYear = mLastActivatedTime.get(Calendar.DAY_OF_YEAR);
465                     final int hourOfDay = mLastActivatedTime.get(Calendar.HOUR_OF_DAY);
466                     final int minute = mLastActivatedTime.get(Calendar.MINUTE);
467 
468                     mLastActivatedTime.setTimeZone(currentTimeZone);
469                     mLastActivatedTime.set(Calendar.YEAR, year);
470                     mLastActivatedTime.set(Calendar.DAY_OF_YEAR, dayOfYear);
471                     mLastActivatedTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
472                     mLastActivatedTime.set(Calendar.MINUTE, minute);
473                 }
474 
475                 // Maintain the existing activated state if within the current period.
476                 if (mLastActivatedTime.before(now)
477                         && mLastActivatedTime.after(startTime)
478                         && (mLastActivatedTime.after(endTime) || now.before(endTime))) {
479                     activate = mController.isActivated();
480                 }
481             }
482 
483             if (mIsActivated == null || mIsActivated != activate) {
484                 mController.setActivated(activate);
485             }
486             updateNextAlarm(mIsActivated, now);
487         }
488 
updateNextAlarm(@ullable Boolean activated, @NonNull Calendar now)489         private void updateNextAlarm(@Nullable Boolean activated, @NonNull Calendar now) {
490             if (activated != null) {
491                 final Calendar next = activated ? mEndTime.getDateTimeAfter(now)
492                         : mStartTime.getDateTimeAfter(now);
493                 mAlarmManager.setExact(AlarmManager.RTC, next.getTimeInMillis(), TAG, this, null);
494             }
495         }
496 
497         @Override
onStart()498         public void onStart() {
499             final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
500             intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
501             getContext().registerReceiver(mTimeChangedReceiver, intentFilter);
502 
503             mStartTime = mController.getCustomStartTime();
504             mEndTime = mController.getCustomEndTime();
505 
506             mLastActivatedTime = mController.getLastActivatedTime();
507 
508             // Force an update to initialize state.
509             updateActivated();
510         }
511 
512         @Override
onStop()513         public void onStop() {
514             getContext().unregisterReceiver(mTimeChangedReceiver);
515 
516             mAlarmManager.cancel(this);
517             mLastActivatedTime = null;
518         }
519 
520         @Override
onActivated(boolean activated)521         public void onActivated(boolean activated) {
522             mLastActivatedTime = mController.getLastActivatedTime();
523             updateNextAlarm(activated, Calendar.getInstance());
524         }
525 
526         @Override
onCustomStartTimeChanged(NightDisplayController.LocalTime startTime)527         public void onCustomStartTimeChanged(NightDisplayController.LocalTime startTime) {
528             mStartTime = startTime;
529             mLastActivatedTime = null;
530             updateActivated();
531         }
532 
533         @Override
onCustomEndTimeChanged(NightDisplayController.LocalTime endTime)534         public void onCustomEndTimeChanged(NightDisplayController.LocalTime endTime) {
535             mEndTime = endTime;
536             mLastActivatedTime = null;
537             updateActivated();
538         }
539 
540         @Override
onAlarm()541         public void onAlarm() {
542             Slog.d(TAG, "onAlarm");
543             updateActivated();
544         }
545     }
546 
547     private class TwilightAutoMode extends AutoMode implements TwilightListener {
548 
549         private final TwilightManager mTwilightManager;
550 
TwilightAutoMode()551         TwilightAutoMode() {
552             mTwilightManager = getLocalService(TwilightManager.class);
553         }
554 
updateActivated(TwilightState state)555         private void updateActivated(TwilightState state) {
556             if (state == null) {
557                 // If there isn't a valid TwilightState then just keep the current activated
558                 // state.
559                 return;
560             }
561 
562             boolean activate = state.isNight();
563             final Calendar lastActivatedTime = mController.getLastActivatedTime();
564             if (lastActivatedTime != null) {
565                 final Calendar now = Calendar.getInstance();
566                 final Calendar sunrise = state.sunrise();
567                 final Calendar sunset = state.sunset();
568 
569                 // Maintain the existing activated state if within the current period.
570                 if (lastActivatedTime.before(now)
571                         && (lastActivatedTime.after(sunrise) ^ lastActivatedTime.after(sunset))) {
572                     activate = mController.isActivated();
573                 }
574             }
575 
576             if (mIsActivated == null || mIsActivated != activate) {
577                 mController.setActivated(activate);
578             }
579         }
580 
581         @Override
onStart()582         public void onStart() {
583             mTwilightManager.registerListener(this, mHandler);
584 
585             // Force an update to initialize state.
586             updateActivated(mTwilightManager.getLastTwilightState());
587         }
588 
589         @Override
onStop()590         public void onStop() {
591             mTwilightManager.unregisterListener(this);
592         }
593 
594         @Override
onActivated(boolean activated)595         public void onActivated(boolean activated) {
596         }
597 
598         @Override
onTwilightStateChanged(@ullable TwilightState state)599         public void onTwilightStateChanged(@Nullable TwilightState state) {
600             Slog.d(TAG, "onTwilightStateChanged: isNight="
601                     + (state == null ? null : state.isNight()));
602             updateActivated(state);
603         }
604     }
605 
606     /**
607      * Interpolates between two 4x4 color transform matrices (in column-major order).
608      */
609     private static class ColorMatrixEvaluator implements TypeEvaluator<float[]> {
610 
611         /**
612          * Result matrix returned by {@link #evaluate(float, float[], float[])}.
613          */
614         private final float[] mResultMatrix = new float[16];
615 
616         @Override
evaluate(float fraction, float[] startValue, float[] endValue)617         public float[] evaluate(float fraction, float[] startValue, float[] endValue) {
618             for (int i = 0; i < mResultMatrix.length; i++) {
619                 mResultMatrix[i] = MathUtils.lerp(startValue[i], endValue[i], fraction);
620             }
621             return mResultMatrix;
622         }
623     }
624 }
625