Add color temperature preference for Night Display

Bug: 32463283
Test: adb shell settings put secure night_display_color_temperature
XXXX, where XXXX is {0, 2900, 4000, 7000}, and the temperatures
outside the valid range are capped at the min/max, respectively.
Change-Id: I322c0a907b30742fc312a9938fd0c47f679e580b
diff --git a/services/core/java/com/android/server/display/NightDisplayService.java b/services/core/java/com/android/server/display/NightDisplayService.java
index cba694c..d1275bb 100644
--- a/services/core/java/com/android/server/display/NightDisplayService.java
+++ b/services/core/java/com/android/server/display/NightDisplayService.java
@@ -65,16 +65,6 @@
     private static final boolean DEBUG = false;
 
     /**
-     * Night display ~= 3400 K.
-     */
-    private static final float[] MATRIX_NIGHT = new float[] {
-        1,      0,      0, 0,
-        0, 0.754f,      0, 0,
-        0,      0, 0.516f, 0,
-        0,      0,      0, 1
-    };
-
-    /**
      * The transition time, in milliseconds, for Night Display to turn on/off.
      */
     private static final long TRANSITION_DURATION = 3000L;
@@ -112,13 +102,34 @@
                     if (enabled) {
                         dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, MATRIX_IDENTITY);
                     } else if (mController != null && mController.isActivated()) {
-                        dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, MATRIX_NIGHT);
+                        setMatrix(mController.getColorTemperature(), mMatrixNight);
+                        dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, mMatrixNight);
                     }
                 }
             });
         }
     };
 
+    private float[] mMatrixNight = new float[16];
+
+    /**
+     *  These coefficients were generated by an LLS quadratic regression fitted to the
+     *  overdetermined system based on experimental readings (and subsequent conversion from xy
+     *  chromaticity coordinates to gamma-corrected RGB values): { (temperature, R, G, B) } ->
+     *  { (7304, 1.0, 1.0, 1.0), (4082, 1.0, 0.857, 0.719), (2850, 1.0, .754, .516),
+     *  (2596, 1.0, 0.722, 0.454) }. The 3x3 matrix is formatted like so:
+     *  <table>
+     *      <tr><td>R: a coefficient</td><td>G: a coefficient</td><td>B: a coefficient</td></tr>
+     *      <tr><td>R: b coefficient</td><td>G: b coefficient</td><td>B: b coefficient</td></tr>
+     *      <tr><td>R: y-intercept</td><td>G: y-intercept</td><td>B: y-intercept</td></tr>
+     *  </table>
+     */
+    private static final float[] mColorTempCoefficients = new float[] {
+            0.0f, -0.00000000962353339f, -0.0000000189359041f,
+            0.0f, 0.000153045476f, 0.000302412211f,
+            1.0f, 0.390782778f, -0.198650895f
+    };
+
     private int mCurrentUser = UserHandle.USER_NULL;
     private ContentObserver mUserSetupObserver;
     private boolean mBootCompleted;
@@ -232,6 +243,9 @@
         mController = new NightDisplayController(getContext(), mCurrentUser);
         mController.setListener(this);
 
+        // Prepare color transformation matrix.
+        setMatrix(mController.getColorTemperature(), mMatrixNight);
+
         // Initialize the current auto mode.
         onAutoModeChanged(mController.getAutoMode());
 
@@ -239,6 +253,9 @@
         if (mIsActivated == null) {
             onActivated(mController.isActivated());
         }
+
+        // Transition the screen to the current temperature.
+        applyTint(false);
     }
 
     private void tearDown() {
@@ -273,53 +290,7 @@
 
             mIsActivated = activated;
 
-            // 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 ? MATRIX_NIGHT : null;
-
-            mColorMatrixAnimator = ValueAnimator.ofObject(COLOR_MATRIX_EVALUATOR,
-                    from == null ? MATRIX_IDENTITY : from, to == null ? MATRIX_IDENTITY : 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();
+            applyTint(false);
         }
     }
 
@@ -361,6 +332,97 @@
         }
     }
 
+    @Override
+    public void onColorTemperatureChanged(int colorTemperature) {
+        setMatrix(colorTemperature, mMatrixNight);
+        applyTint(true);
+    }
+
+    /**
+     * 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[3] + mColorTempCoefficients[6];
+        final float green = squareTemperature * mColorTempCoefficients[1]
+                + colorTemperature * mColorTempCoefficients[4] + mColorTempCoefficients[7];
+        final float blue = squareTemperature * mColorTempCoefficients[2]
+                + colorTemperature * mColorTempCoefficients[5] + mColorTempCoefficients[8];
+        outTemp[0] = red;
+        outTemp[5] = green;
+        outTemp[10] = blue;
+    }
+
     private abstract class AutoMode implements NightDisplayController.Callback {
         public abstract void onStart();
         public abstract void onStop();