Rename NightDisplayController and -Service
Bug: 68258004
Test: make -j100 && runtest -x
frameworks/base/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
&& runtest -c com.android.server.ColorDisplayServiceTest
frameworks-services
Change-Id: I2b89942bd412e4d6958e65b62bc345fb1e60176f
diff --git a/services/core/java/com/android/server/display/ColorDisplayService.java b/services/core/java/com/android/server/display/ColorDisplayService.java
new file mode 100644
index 0000000..af8ecad
--- /dev/null
+++ b/services/core/java/com/android/server/display/ColorDisplayService.java
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.opengl.Matrix;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings.Secure;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
+import android.util.MathUtils;
+import android.util.Slog;
+import android.view.animation.AnimationUtils;
+
+import com.android.internal.app.ColorDisplayController;
+import com.android.server.SystemService;
+import com.android.server.twilight.TwilightListener;
+import com.android.server.twilight.TwilightManager;
+import com.android.server.twilight.TwilightState;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.android.internal.R;
+
+import static com.android.server.display.DisplayTransformManager.LEVEL_COLOR_MATRIX_NIGHT_DISPLAY;
+
+/**
+ * Tints the display at night.
+ */
+public final class ColorDisplayService extends SystemService
+ implements ColorDisplayController.Callback {
+
+ private static final String TAG = "ColorDisplayService";
+
+ /**
+ * The transition time, in milliseconds, for Night Display to turn on/off.
+ */
+ private static final long TRANSITION_DURATION = 3000L;
+
+ /**
+ * The identity matrix, used if one of the given matrices is {@code null}.
+ */
+ private static final float[] MATRIX_IDENTITY = new float[16];
+ static {
+ Matrix.setIdentityM(MATRIX_IDENTITY, 0);
+ }
+
+ /**
+ * Evaluator used to animate color matrix transitions.
+ */
+ private static final ColorMatrixEvaluator COLOR_MATRIX_EVALUATOR = new ColorMatrixEvaluator();
+
+ private final Handler mHandler;
+ private final AtomicBoolean mIgnoreAllColorMatrixChanges = new AtomicBoolean();
+ private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
+ @Override
+ public void onVrStateChanged(final boolean enabled) {
+ // Turn off all night mode display stuff while device is in VR mode.
+ mIgnoreAllColorMatrixChanges.set(enabled);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Cancel in-progress animations
+ if (mColorMatrixAnimator != null) {
+ mColorMatrixAnimator.cancel();
+ }
+
+ final DisplayTransformManager dtm =
+ getLocalService(DisplayTransformManager.class);
+ if (enabled) {
+ dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, MATRIX_IDENTITY);
+ } else if (mController != null && mController.isActivated()) {
+ setMatrix(mController.getColorTemperature(), mMatrixNight);
+ dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, mMatrixNight);
+ }
+ }
+ });
+ }
+ };
+
+ private float[] mMatrixNight = new float[16];
+
+ private final float[] mColorTempCoefficients = new float[9];
+
+ private int mCurrentUser = UserHandle.USER_NULL;
+ private ContentObserver mUserSetupObserver;
+ private boolean mBootCompleted;
+
+ private ColorDisplayController mController;
+ private ValueAnimator mColorMatrixAnimator;
+ private Boolean mIsActivated;
+ private AutoMode mAutoMode;
+
+ public ColorDisplayService(Context context) {
+ super(context);
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void onStart() {
+ // Nothing to publish.
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase >= PHASE_SYSTEM_SERVICES_READY) {
+ final IVrManager vrManager = IVrManager.Stub.asInterface(
+ getBinderService(Context.VR_SERVICE));
+ if (vrManager != null) {
+ try {
+ vrManager.registerListener(mVrStateCallbacks);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to register VR mode state listener: " + e);
+ }
+ }
+ }
+
+ if (phase >= PHASE_BOOT_COMPLETED) {
+ mBootCompleted = true;
+
+ // Register listeners now that boot is complete.
+ if (mCurrentUser != UserHandle.USER_NULL && mUserSetupObserver == null) {
+ setUp();
+ }
+ }
+ }
+
+ @Override
+ public void onStartUser(int userHandle) {
+ super.onStartUser(userHandle);
+
+ if (mCurrentUser == UserHandle.USER_NULL) {
+ onUserChanged(userHandle);
+ }
+ }
+
+ @Override
+ public void onSwitchUser(int userHandle) {
+ super.onSwitchUser(userHandle);
+
+ onUserChanged(userHandle);
+ }
+
+ @Override
+ public void onStopUser(int userHandle) {
+ super.onStopUser(userHandle);
+
+ if (mCurrentUser == userHandle) {
+ onUserChanged(UserHandle.USER_NULL);
+ }
+ }
+
+ private void onUserChanged(int userHandle) {
+ final ContentResolver cr = getContext().getContentResolver();
+
+ if (mCurrentUser != UserHandle.USER_NULL) {
+ if (mUserSetupObserver != null) {
+ cr.unregisterContentObserver(mUserSetupObserver);
+ mUserSetupObserver = null;
+ } else if (mBootCompleted) {
+ tearDown();
+ }
+ }
+
+ mCurrentUser = userHandle;
+
+ if (mCurrentUser != UserHandle.USER_NULL) {
+ if (!isUserSetupCompleted(cr, mCurrentUser)) {
+ mUserSetupObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (isUserSetupCompleted(cr, mCurrentUser)) {
+ cr.unregisterContentObserver(this);
+ mUserSetupObserver = null;
+
+ if (mBootCompleted) {
+ setUp();
+ }
+ }
+ }
+ };
+ cr.registerContentObserver(Secure.getUriFor(Secure.USER_SETUP_COMPLETE),
+ false /* notifyForDescendents */, mUserSetupObserver, mCurrentUser);
+ } else if (mBootCompleted) {
+ setUp();
+ }
+ }
+ }
+
+ private static boolean isUserSetupCompleted(ContentResolver cr, int userHandle) {
+ return Secure.getIntForUser(cr, Secure.USER_SETUP_COMPLETE, 0, userHandle) == 1;
+ }
+
+ private void setUp() {
+ Slog.d(TAG, "setUp: currentUser=" + mCurrentUser);
+
+ // Create a new controller for the current user and start listening for changes.
+ mController = new ColorDisplayController(getContext(), mCurrentUser);
+ mController.setListener(this);
+
+ setCoefficientMatrix(getContext());
+
+ // Prepare color transformation matrix.
+ setMatrix(mController.getColorTemperature(), mMatrixNight);
+
+ // Initialize the current auto mode.
+ onAutoModeChanged(mController.getAutoMode());
+
+ // Force the initialization current activated state.
+ if (mIsActivated == null) {
+ onActivated(mController.isActivated());
+ }
+
+ // Transition the screen to the current temperature.
+ applyTint(false);
+ }
+
+ private void tearDown() {
+ Slog.d(TAG, "tearDown: currentUser=" + mCurrentUser);
+
+ if (mController != null) {
+ mController.setListener(null);
+ mController = null;
+ }
+
+ if (mAutoMode != null) {
+ mAutoMode.onStop();
+ mAutoMode = null;
+ }
+
+ if (mColorMatrixAnimator != null) {
+ mColorMatrixAnimator.end();
+ mColorMatrixAnimator = null;
+ }
+
+ mIsActivated = null;
+ }
+
+ @Override
+ public void onActivated(boolean activated) {
+ if (mIsActivated == null || mIsActivated != activated) {
+ Slog.i(TAG, activated ? "Turning on night display" : "Turning off night display");
+
+ mIsActivated = activated;
+
+ if (mAutoMode != null) {
+ mAutoMode.onActivated(activated);
+ }
+
+ applyTint(false);
+ }
+ }
+
+ @Override
+ public void onAutoModeChanged(int autoMode) {
+ Slog.d(TAG, "onAutoModeChanged: autoMode=" + autoMode);
+
+ if (mAutoMode != null) {
+ mAutoMode.onStop();
+ mAutoMode = null;
+ }
+
+ if (autoMode == ColorDisplayController.AUTO_MODE_CUSTOM) {
+ mAutoMode = new CustomAutoMode();
+ } else if (autoMode == ColorDisplayController.AUTO_MODE_TWILIGHT) {
+ mAutoMode = new TwilightAutoMode();
+ }
+
+ if (mAutoMode != null) {
+ mAutoMode.onStart();
+ }
+ }
+
+ @Override
+ public void onCustomStartTimeChanged(LocalTime startTime) {
+ Slog.d(TAG, "onCustomStartTimeChanged: startTime=" + startTime);
+
+ if (mAutoMode != null) {
+ mAutoMode.onCustomStartTimeChanged(startTime);
+ }
+ }
+
+ @Override
+ public void onCustomEndTimeChanged(LocalTime endTime) {
+ Slog.d(TAG, "onCustomEndTimeChanged: endTime=" + endTime);
+
+ if (mAutoMode != null) {
+ mAutoMode.onCustomEndTimeChanged(endTime);
+ }
+ }
+
+ @Override
+ public void onColorTemperatureChanged(int colorTemperature) {
+ setMatrix(colorTemperature, mMatrixNight);
+ applyTint(true);
+ }
+
+ @Override
+ public void onDisplayColorModeChanged(int colorMode) {
+ final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class);
+ dtm.setColorMode(colorMode);
+
+ setCoefficientMatrix(getContext());
+ setMatrix(mController.getColorTemperature(), mMatrixNight);
+ applyTint(true);
+ }
+
+ private void setCoefficientMatrix(Context context) {
+ final boolean isNative = DisplayTransformManager.isNativeModeEnabled();
+ final String[] coefficients = context.getResources().getStringArray(isNative
+ ? R.array.config_nightDisplayColorTemperatureCoefficientsNative
+ : R.array.config_nightDisplayColorTemperatureCoefficients);
+ for (int i = 0; i < 9 && i < coefficients.length; i++) {
+ mColorTempCoefficients[i] = Float.parseFloat(coefficients[i]);
+ }
+ }
+
+ /**
+ * Applies current color temperature matrix, or removes it if deactivated.
+ *
+ * @param immediate {@code true} skips transition animation
+ */
+ private void applyTint(boolean immediate) {
+ // Cancel the old animator if still running.
+ if (mColorMatrixAnimator != null) {
+ mColorMatrixAnimator.cancel();
+ }
+
+ // Don't do any color matrix change animations if we are ignoring them anyway.
+ if (mIgnoreAllColorMatrixChanges.get()) {
+ return;
+ }
+
+ final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class);
+ final float[] from = dtm.getColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY);
+ final float[] to = mIsActivated ? mMatrixNight : MATRIX_IDENTITY;
+
+ if (immediate) {
+ dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, to);
+ } else {
+ mColorMatrixAnimator = ValueAnimator.ofObject(COLOR_MATRIX_EVALUATOR,
+ from == null ? MATRIX_IDENTITY : from, to);
+ mColorMatrixAnimator.setDuration(TRANSITION_DURATION);
+ mColorMatrixAnimator.setInterpolator(AnimationUtils.loadInterpolator(
+ getContext(), android.R.interpolator.fast_out_slow_in));
+ mColorMatrixAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ final float[] value = (float[]) animator.getAnimatedValue();
+ dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, value);
+ }
+ });
+ mColorMatrixAnimator.addListener(new AnimatorListenerAdapter() {
+
+ private boolean mIsCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ mIsCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ if (!mIsCancelled) {
+ // Ensure final color matrix is set at the end of the animation. If the
+ // animation is cancelled then don't set the final color matrix so the new
+ // animator can pick up from where this one left off.
+ dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, to);
+ }
+ mColorMatrixAnimator = null;
+ }
+ });
+ mColorMatrixAnimator.start();
+ }
+ }
+
+ /**
+ * Set the color transformation {@code MATRIX_NIGHT} to the given color temperature.
+ *
+ * @param colorTemperature color temperature in Kelvin
+ * @param outTemp the 4x4 display transformation matrix for that color temperature
+ */
+ private void setMatrix(int colorTemperature, float[] outTemp) {
+ if (outTemp.length != 16) {
+ Slog.d(TAG, "The display transformation matrix must be 4x4");
+ return;
+ }
+
+ Matrix.setIdentityM(mMatrixNight, 0);
+
+ final float squareTemperature = colorTemperature * colorTemperature;
+ final float red = squareTemperature * mColorTempCoefficients[0]
+ + colorTemperature * mColorTempCoefficients[1] + mColorTempCoefficients[2];
+ final float green = squareTemperature * mColorTempCoefficients[3]
+ + colorTemperature * mColorTempCoefficients[4] + mColorTempCoefficients[5];
+ final float blue = squareTemperature * mColorTempCoefficients[6]
+ + colorTemperature * mColorTempCoefficients[7] + mColorTempCoefficients[8];
+ outTemp[0] = red;
+ outTemp[5] = green;
+ outTemp[10] = blue;
+ }
+
+ /**
+ * Returns the first date time corresponding to the local time that occurs before the
+ * provided date time.
+ *
+ * @param compareTime the LocalDateTime to compare against
+ * @return the prior LocalDateTime corresponding to this local time
+ */
+ public static LocalDateTime getDateTimeBefore(LocalTime localTime, LocalDateTime compareTime) {
+ final LocalDateTime ldt = LocalDateTime.of(compareTime.getYear(), compareTime.getMonth(),
+ compareTime.getDayOfMonth(), localTime.getHour(), localTime.getMinute());
+
+ // Check if the local time has passed, if so return the same time yesterday.
+ return ldt.isAfter(compareTime) ? ldt.minusDays(1) : ldt;
+ }
+
+ /**
+ * Returns the first date time corresponding to this local time that occurs after the
+ * provided date time.
+ *
+ * @param compareTime the LocalDateTime to compare against
+ * @return the next LocalDateTime corresponding to this local time
+ */
+ public static LocalDateTime getDateTimeAfter(LocalTime localTime, LocalDateTime compareTime) {
+ final LocalDateTime ldt = LocalDateTime.of(compareTime.getYear(), compareTime.getMonth(),
+ compareTime.getDayOfMonth(), localTime.getHour(), localTime.getMinute());
+
+ // Check if the local time has passed, if so return the same time tomorrow.
+ return ldt.isBefore(compareTime) ? ldt.plusDays(1) : ldt;
+ }
+
+ private abstract class AutoMode implements ColorDisplayController.Callback {
+ public abstract void onStart();
+
+ public abstract void onStop();
+ }
+
+ private class CustomAutoMode extends AutoMode implements AlarmManager.OnAlarmListener {
+
+ private final AlarmManager mAlarmManager;
+ private final BroadcastReceiver mTimeChangedReceiver;
+
+ private LocalTime mStartTime;
+ private LocalTime mEndTime;
+
+ private LocalDateTime mLastActivatedTime;
+
+ CustomAutoMode() {
+ mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
+ mTimeChangedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateActivated();
+ }
+ };
+ }
+
+ private void updateActivated() {
+ final LocalDateTime now = LocalDateTime.now();
+ final LocalDateTime start = getDateTimeBefore(mStartTime, now);
+ final LocalDateTime end = getDateTimeAfter(mEndTime, start);
+ boolean activate = now.isBefore(end);
+
+ if (mLastActivatedTime != null) {
+ // Maintain the existing activated state if within the current period.
+ if (mLastActivatedTime.isBefore(now) && mLastActivatedTime.isAfter(start)
+ && (mLastActivatedTime.isAfter(end) || now.isBefore(end))) {
+ activate = mController.isActivated();
+ }
+ }
+
+ if (mIsActivated == null || mIsActivated != activate) {
+ mController.setActivated(activate);
+ }
+
+ updateNextAlarm(mIsActivated, now);
+ }
+
+ private void updateNextAlarm(@Nullable Boolean activated, @NonNull LocalDateTime now) {
+ if (activated != null) {
+ final LocalDateTime next = activated ? getDateTimeAfter(mEndTime, now)
+ : getDateTimeAfter(mStartTime, now);
+ final long millis = next.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ mAlarmManager.setExact(AlarmManager.RTC, millis, TAG, this, null);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
+ intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ getContext().registerReceiver(mTimeChangedReceiver, intentFilter);
+
+ mStartTime = mController.getCustomStartTime();
+ mEndTime = mController.getCustomEndTime();
+
+ mLastActivatedTime = mController.getLastActivatedTime();
+
+ // Force an update to initialize state.
+ updateActivated();
+ }
+
+ @Override
+ public void onStop() {
+ getContext().unregisterReceiver(mTimeChangedReceiver);
+
+ mAlarmManager.cancel(this);
+ mLastActivatedTime = null;
+ }
+
+ @Override
+ public void onActivated(boolean activated) {
+ mLastActivatedTime = mController.getLastActivatedTime();
+ updateNextAlarm(activated, LocalDateTime.now());
+ }
+
+ @Override
+ public void onCustomStartTimeChanged(LocalTime startTime) {
+ mStartTime = startTime;
+ mLastActivatedTime = null;
+ updateActivated();
+ }
+
+ @Override
+ public void onCustomEndTimeChanged(LocalTime endTime) {
+ mEndTime = endTime;
+ mLastActivatedTime = null;
+ updateActivated();
+ }
+
+ @Override
+ public void onAlarm() {
+ Slog.d(TAG, "onAlarm");
+ updateActivated();
+ }
+ }
+
+ private class TwilightAutoMode extends AutoMode implements TwilightListener {
+
+ private final TwilightManager mTwilightManager;
+
+ TwilightAutoMode() {
+ mTwilightManager = getLocalService(TwilightManager.class);
+ }
+
+ private void updateActivated(TwilightState state) {
+ if (state == null) {
+ // If there isn't a valid TwilightState then just keep the current activated
+ // state.
+ return;
+ }
+
+ boolean activate = state.isNight();
+ final LocalDateTime lastActivatedTime = mController.getLastActivatedTime();
+ if (lastActivatedTime != null) {
+ final LocalDateTime now = LocalDateTime.now();
+ final LocalDateTime sunrise = state.sunrise();
+ final LocalDateTime sunset = state.sunset();
+ // Maintain the existing activated state if within the current period.
+ if (lastActivatedTime.isBefore(now) && (lastActivatedTime.isBefore(sunrise)
+ ^ lastActivatedTime.isBefore(sunset))) {
+ activate = mController.isActivated();
+ }
+ }
+
+ if (mIsActivated == null || mIsActivated != activate) {
+ mController.setActivated(activate);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ mTwilightManager.registerListener(this, mHandler);
+
+ // Force an update to initialize state.
+ updateActivated(mTwilightManager.getLastTwilightState());
+ }
+
+ @Override
+ public void onStop() {
+ mTwilightManager.unregisterListener(this);
+ }
+
+ @Override
+ public void onActivated(boolean activated) {
+ }
+
+ @Override
+ public void onTwilightStateChanged(@Nullable TwilightState state) {
+ Slog.d(TAG, "onTwilightStateChanged: isNight="
+ + (state == null ? null : state.isNight()));
+ updateActivated(state);
+ }
+ }
+
+ /**
+ * Interpolates between two 4x4 color transform matrices (in column-major order).
+ */
+ private static class ColorMatrixEvaluator implements TypeEvaluator<float[]> {
+
+ /**
+ * Result matrix returned by {@link #evaluate(float, float[], float[])}.
+ */
+ private final float[] mResultMatrix = new float[16];
+
+ @Override
+ public float[] evaluate(float fraction, float[] startValue, float[] endValue) {
+ for (int i = 0; i < mResultMatrix.length; i++) {
+ mResultMatrix[i] = MathUtils.lerp(startValue[i], endValue[i], fraction);
+ }
+ return mResultMatrix;
+ }
+ }
+}