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
17package com.android.server.display;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.TypeEvaluator;
22import android.animation.ValueAnimator;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.app.AlarmManager;
26import android.content.BroadcastReceiver;
27import android.content.ContentResolver;
28import android.content.Context;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.database.ContentObserver;
32import android.net.Uri;
33import android.opengl.Matrix;
34import android.os.Handler;
35import android.os.Looper;
36import android.os.RemoteException;
37import android.os.UserHandle;
38import android.provider.Settings.Secure;
39import android.service.vr.IVrManager;
40import android.service.vr.IVrStateCallbacks;
41import android.util.MathUtils;
42import android.util.Slog;
43import android.view.animation.AnimationUtils;
44
45import com.android.internal.app.NightDisplayController;
46import com.android.server.SystemService;
47import com.android.server.twilight.TwilightListener;
48import com.android.server.twilight.TwilightManager;
49import com.android.server.twilight.TwilightState;
50import com.android.server.vr.VrManagerService;
51
52import java.util.concurrent.atomic.AtomicBoolean;
53import java.util.Calendar;
54import java.util.TimeZone;
55
56import static com.android.server.display.DisplayTransformManager.LEVEL_COLOR_MATRIX_NIGHT_DISPLAY;
57
58/**
59 * Tints the display at night.
60 */
61public final class NightDisplayService extends SystemService
62        implements NightDisplayController.Callback {
63
64    private static final String TAG = "NightDisplayService";
65    private static final boolean DEBUG = false;
66
67    /**
68     * Night display ~= 3400 K.
69     */
70    private static final float[] MATRIX_NIGHT = new float[] {
71        1,      0,      0, 0,
72        0, 0.754f,      0, 0,
73        0,      0, 0.516f, 0,
74        0,      0,      0, 1
75    };
76
77    /**
78     * The identity matrix, used if one of the given matrices is {@code null}.
79     */
80    private static final float[] MATRIX_IDENTITY = new float[16];
81    static {
82        Matrix.setIdentityM(MATRIX_IDENTITY, 0);
83    }
84
85    /**
86     * Evaluator used to animate color matrix transitions.
87     */
88    private static final ColorMatrixEvaluator COLOR_MATRIX_EVALUATOR = new ColorMatrixEvaluator();
89
90    private final Handler mHandler;
91    private final AtomicBoolean mIgnoreAllColorMatrixChanges = new AtomicBoolean();
92    private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
93        @Override
94        public void onVrStateChanged(final boolean enabled) {
95            // Turn off all night mode display stuff while device is in VR mode.
96            mIgnoreAllColorMatrixChanges.set(enabled);
97            mHandler.post(new Runnable() {
98                @Override
99                public void run() {
100                    // Cancel in-progress animations
101                    if (mColorMatrixAnimator != null) {
102                        mColorMatrixAnimator.cancel();
103                    }
104
105                    final DisplayTransformManager dtm =
106                            getLocalService(DisplayTransformManager.class);
107                    if (enabled) {
108                        dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, MATRIX_IDENTITY);
109                    } else if (mController.isActivated()) {
110                        dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, MATRIX_NIGHT);
111                    }
112                }
113            });
114        }
115    };
116
117    private int mCurrentUser = UserHandle.USER_NULL;
118    private ContentObserver mUserSetupObserver;
119    private boolean mBootCompleted;
120
121    private NightDisplayController mController;
122    private ValueAnimator mColorMatrixAnimator;
123    private Boolean mIsActivated;
124    private AutoMode mAutoMode;
125
126    public NightDisplayService(Context context) {
127        super(context);
128        mHandler = new Handler(Looper.getMainLooper());
129    }
130
131    @Override
132    public void onStart() {
133        // Nothing to publish.
134    }
135
136    @Override
137    public void onBootPhase(int phase) {
138        if (phase == PHASE_SYSTEM_SERVICES_READY) {
139            IVrManager vrManager =
140                    (IVrManager) getBinderService(VrManagerService.VR_MANAGER_BINDER_SERVICE);
141            if (vrManager != null) {
142                try {
143                    vrManager.registerListener(mVrStateCallbacks);
144                } catch (RemoteException e) {
145                    Slog.e(TAG, "Failed to register VR mode state listener: " + e);
146                }
147            }
148        } else if (phase == PHASE_BOOT_COMPLETED) {
149            mBootCompleted = true;
150
151            // Register listeners now that boot is complete.
152            if (mCurrentUser != UserHandle.USER_NULL && mUserSetupObserver == null) {
153                setUp();
154            }
155        }
156    }
157
158    @Override
159    public void onStartUser(int userHandle) {
160        super.onStartUser(userHandle);
161
162        if (mCurrentUser == UserHandle.USER_NULL) {
163            onUserChanged(userHandle);
164        }
165    }
166
167    @Override
168    public void onSwitchUser(int userHandle) {
169        super.onSwitchUser(userHandle);
170
171        onUserChanged(userHandle);
172    }
173
174    @Override
175    public void onStopUser(int userHandle) {
176        super.onStopUser(userHandle);
177
178        if (mCurrentUser == userHandle) {
179            onUserChanged(UserHandle.USER_NULL);
180        }
181    }
182
183    private void onUserChanged(int userHandle) {
184        final ContentResolver cr = getContext().getContentResolver();
185
186        if (mCurrentUser != UserHandle.USER_NULL) {
187            if (mUserSetupObserver != null) {
188                cr.unregisterContentObserver(mUserSetupObserver);
189                mUserSetupObserver = null;
190            } else if (mBootCompleted) {
191                tearDown();
192            }
193        }
194
195        mCurrentUser = userHandle;
196
197        if (mCurrentUser != UserHandle.USER_NULL) {
198            if (!isUserSetupCompleted(cr, mCurrentUser)) {
199                mUserSetupObserver = new ContentObserver(mHandler) {
200                    @Override
201                    public void onChange(boolean selfChange, Uri uri) {
202                        if (isUserSetupCompleted(cr, mCurrentUser)) {
203                            cr.unregisterContentObserver(this);
204                            mUserSetupObserver = null;
205
206                            if (mBootCompleted) {
207                                setUp();
208                            }
209                        }
210                    }
211                };
212                cr.registerContentObserver(Secure.getUriFor(Secure.USER_SETUP_COMPLETE),
213                        false /* notifyForDescendents */, mUserSetupObserver, mCurrentUser);
214            } else if (mBootCompleted) {
215                setUp();
216            }
217        }
218    }
219
220    private static boolean isUserSetupCompleted(ContentResolver cr, int userHandle) {
221        return Secure.getIntForUser(cr, Secure.USER_SETUP_COMPLETE, 0, userHandle) == 1;
222    }
223
224    private void setUp() {
225        Slog.d(TAG, "setUp: currentUser=" + mCurrentUser);
226
227        // Create a new controller for the current user and start listening for changes.
228        mController = new NightDisplayController(getContext(), mCurrentUser);
229        mController.setListener(this);
230
231        // Initialize the current auto mode.
232        onAutoModeChanged(mController.getAutoMode());
233
234        // Force the initialization current activated state.
235        if (mIsActivated == null) {
236            onActivated(mController.isActivated());
237        }
238    }
239
240    private void tearDown() {
241        Slog.d(TAG, "tearDown: currentUser=" + mCurrentUser);
242
243        if (mController != null) {
244            mController.setListener(null);
245            mController = null;
246        }
247
248        if (mAutoMode != null) {
249            mAutoMode.onStop();
250            mAutoMode = null;
251        }
252
253        if (mColorMatrixAnimator != null) {
254            mColorMatrixAnimator.end();
255            mColorMatrixAnimator = null;
256        }
257
258        mIsActivated = null;
259    }
260
261    @Override
262    public void onActivated(boolean activated) {
263        if (mIsActivated == null || mIsActivated != activated) {
264            Slog.i(TAG, activated ? "Turning on night display" : "Turning off night display");
265
266            if (mAutoMode != null) {
267                mAutoMode.onActivated(activated);
268            }
269
270            mIsActivated = activated;
271
272            // Cancel the old animator if still running.
273            if (mColorMatrixAnimator != null) {
274                mColorMatrixAnimator.cancel();
275            }
276
277            // Don't do any color matrix change animations if we are ignoring them anyway.
278            if (mIgnoreAllColorMatrixChanges.get()) {
279                return;
280            }
281
282            final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class);
283            final float[] from = dtm.getColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY);
284            final float[] to = mIsActivated ? MATRIX_NIGHT : null;
285
286            mColorMatrixAnimator = ValueAnimator.ofObject(COLOR_MATRIX_EVALUATOR,
287                    from == null ? MATRIX_IDENTITY : from, to == null ? MATRIX_IDENTITY : to);
288            mColorMatrixAnimator.setDuration(getContext().getResources()
289                    .getInteger(android.R.integer.config_longAnimTime));
290            mColorMatrixAnimator.setInterpolator(AnimationUtils.loadInterpolator(
291                    getContext(), android.R.interpolator.fast_out_slow_in));
292            mColorMatrixAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
293                @Override
294                public void onAnimationUpdate(ValueAnimator animator) {
295                    final float[] value = (float[]) animator.getAnimatedValue();
296                    dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, value);
297                }
298            });
299            mColorMatrixAnimator.addListener(new AnimatorListenerAdapter() {
300
301                private boolean mIsCancelled;
302
303                @Override
304                public void onAnimationCancel(Animator animator) {
305                    mIsCancelled = true;
306                }
307
308                @Override
309                public void onAnimationEnd(Animator animator) {
310                    if (!mIsCancelled) {
311                        // Ensure final color matrix is set at the end of the animation. If the
312                        // animation is cancelled then don't set the final color matrix so the new
313                        // animator can pick up from where this one left off.
314                        dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, to);
315                    }
316                    mColorMatrixAnimator = null;
317                }
318            });
319            mColorMatrixAnimator.start();
320        }
321    }
322
323    @Override
324    public void onAutoModeChanged(int autoMode) {
325        Slog.d(TAG, "onAutoModeChanged: autoMode=" + autoMode);
326
327        if (mAutoMode != null) {
328            mAutoMode.onStop();
329            mAutoMode = null;
330        }
331
332        if (autoMode == NightDisplayController.AUTO_MODE_CUSTOM) {
333            mAutoMode = new CustomAutoMode();
334        } else if (autoMode == NightDisplayController.AUTO_MODE_TWILIGHT) {
335            mAutoMode = new TwilightAutoMode();
336        }
337
338        if (mAutoMode != null) {
339            mAutoMode.onStart();
340        }
341    }
342
343    @Override
344    public void onCustomStartTimeChanged(NightDisplayController.LocalTime startTime) {
345        Slog.d(TAG, "onCustomStartTimeChanged: startTime=" + startTime);
346
347        if (mAutoMode != null) {
348            mAutoMode.onCustomStartTimeChanged(startTime);
349        }
350    }
351
352    @Override
353    public void onCustomEndTimeChanged(NightDisplayController.LocalTime endTime) {
354        Slog.d(TAG, "onCustomEndTimeChanged: endTime=" + endTime);
355
356        if (mAutoMode != null) {
357            mAutoMode.onCustomEndTimeChanged(endTime);
358        }
359    }
360
361    private abstract class AutoMode implements NightDisplayController.Callback {
362        public abstract void onStart();
363        public abstract void onStop();
364    }
365
366    private class CustomAutoMode extends AutoMode implements AlarmManager.OnAlarmListener {
367
368        private final AlarmManager mAlarmManager;
369        private final BroadcastReceiver mTimeChangedReceiver;
370
371        private NightDisplayController.LocalTime mStartTime;
372        private NightDisplayController.LocalTime mEndTime;
373
374        private Calendar mLastActivatedTime;
375
376        public CustomAutoMode() {
377            mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
378            mTimeChangedReceiver = new BroadcastReceiver() {
379                @Override
380                public void onReceive(Context context, Intent intent) {
381                    updateActivated();
382                }
383            };
384        }
385
386        private void updateActivated() {
387            final Calendar now = Calendar.getInstance();
388            final Calendar startTime = mStartTime.getDateTimeBefore(now);
389            final Calendar endTime = mEndTime.getDateTimeAfter(startTime);
390            final boolean activated = now.before(endTime);
391
392            boolean setActivated = mIsActivated == null || mLastActivatedTime == null;
393            if (!setActivated && mIsActivated != activated) {
394                final TimeZone currentTimeZone = now.getTimeZone();
395                if (!currentTimeZone.equals(mLastActivatedTime.getTimeZone())) {
396                    final int year = mLastActivatedTime.get(Calendar.YEAR);
397                    final int dayOfYear = mLastActivatedTime.get(Calendar.DAY_OF_YEAR);
398                    final int hourOfDay = mLastActivatedTime.get(Calendar.HOUR_OF_DAY);
399                    final int minute = mLastActivatedTime.get(Calendar.MINUTE);
400
401                    mLastActivatedTime.setTimeZone(currentTimeZone);
402                    mLastActivatedTime.set(Calendar.YEAR, year);
403                    mLastActivatedTime.set(Calendar.DAY_OF_YEAR, dayOfYear);
404                    mLastActivatedTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
405                    mLastActivatedTime.set(Calendar.MINUTE, minute);
406                }
407
408                if (mIsActivated) {
409                    setActivated = now.before(mStartTime.getDateTimeBefore(mLastActivatedTime))
410                            || now.after(mEndTime.getDateTimeAfter(mLastActivatedTime));
411                } else {
412                    setActivated = now.before(mEndTime.getDateTimeBefore(mLastActivatedTime))
413                            || now.after(mStartTime.getDateTimeAfter(mLastActivatedTime));
414                }
415            }
416
417            if (setActivated) {
418                mController.setActivated(activated);
419            }
420            updateNextAlarm(mIsActivated, now);
421        }
422
423        private void updateNextAlarm(@Nullable Boolean activated, @NonNull Calendar now) {
424            if (activated != null) {
425                final Calendar next = activated ? mEndTime.getDateTimeAfter(now)
426                        : mStartTime.getDateTimeAfter(now);
427                mAlarmManager.setExact(AlarmManager.RTC, next.getTimeInMillis(), TAG, this, null);
428            }
429        }
430
431        @Override
432        public void onStart() {
433            final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
434            intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
435            getContext().registerReceiver(mTimeChangedReceiver, intentFilter);
436
437            mStartTime = mController.getCustomStartTime();
438            mEndTime = mController.getCustomEndTime();
439
440            // Force an update to initialize state.
441            updateActivated();
442        }
443
444        @Override
445        public void onStop() {
446            getContext().unregisterReceiver(mTimeChangedReceiver);
447
448            mAlarmManager.cancel(this);
449            mLastActivatedTime = null;
450        }
451
452        @Override
453        public void onActivated(boolean activated) {
454            final Calendar now = Calendar.getInstance();
455            if (mIsActivated != null) {
456                mLastActivatedTime = now;
457            }
458            updateNextAlarm(activated, now);
459        }
460
461        @Override
462        public void onCustomStartTimeChanged(NightDisplayController.LocalTime startTime) {
463            mStartTime = startTime;
464            mLastActivatedTime = null;
465            updateActivated();
466        }
467
468        @Override
469        public void onCustomEndTimeChanged(NightDisplayController.LocalTime endTime) {
470            mEndTime = endTime;
471            mLastActivatedTime = null;
472            updateActivated();
473        }
474
475        @Override
476        public void onAlarm() {
477            Slog.d(TAG, "onAlarm");
478            updateActivated();
479        }
480    }
481
482    private class TwilightAutoMode extends AutoMode implements TwilightListener {
483
484        private final TwilightManager mTwilightManager;
485
486        private Calendar mLastActivatedTime;
487
488        public TwilightAutoMode() {
489            mTwilightManager = getLocalService(TwilightManager.class);
490        }
491
492        private void updateActivated(TwilightState state) {
493            final boolean isNight = state != null && state.isNight();
494            boolean setActivated = mIsActivated == null || mIsActivated != isNight;
495            if (setActivated && state != null && mLastActivatedTime != null) {
496                final Calendar sunrise = state.sunrise();
497                final Calendar sunset = state.sunset();
498                if (sunrise.before(sunset)) {
499                    setActivated = mLastActivatedTime.before(sunrise)
500                            || mLastActivatedTime.after(sunset);
501                } else {
502                    setActivated = mLastActivatedTime.before(sunset)
503                            || mLastActivatedTime.after(sunrise);
504                }
505            }
506
507            if (setActivated) {
508                mController.setActivated(isNight);
509            }
510        }
511
512        @Override
513        public void onStart() {
514            mTwilightManager.registerListener(this, mHandler);
515
516            // Force an update to initialize state.
517            updateActivated(mTwilightManager.getLastTwilightState());
518        }
519
520        @Override
521        public void onStop() {
522            mTwilightManager.unregisterListener(this);
523            mLastActivatedTime = null;
524        }
525
526        @Override
527        public void onActivated(boolean activated) {
528            if (mIsActivated != null) {
529                mLastActivatedTime = Calendar.getInstance();
530            }
531        }
532
533        @Override
534        public void onTwilightStateChanged(@Nullable TwilightState state) {
535            Slog.d(TAG, "onTwilightStateChanged: isNight="
536                    + (state == null ? null : state.isNight()));
537            updateActivated(state);
538        }
539    }
540
541    /**
542     * Interpolates between two 4x4 color transform matrices (in column-major order).
543     */
544    private static class ColorMatrixEvaluator implements TypeEvaluator<float[]> {
545
546        /**
547         * Result matrix returned by {@link #evaluate(float, float[], float[])}.
548         */
549        private final float[] mResultMatrix = new float[16];
550
551        @Override
552        public float[] evaluate(float fraction, float[] startValue, float[] endValue) {
553            for (int i = 0; i < mResultMatrix.length; i++) {
554                mResultMatrix[i] = MathUtils.lerp(startValue[i], endValue[i], fraction);
555            }
556            return mResultMatrix;
557        }
558    }
559}
560