summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/res/res/values/config.xml101
-rw-r--r--core/res/res/values/symbols.xml16
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java16
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerShellCommand.java38
-rw-r--r--services/core/java/com/android/server/display/DisplayPowerController.java67
-rw-r--r--services/core/java/com/android/server/display/utils/History.java101
-rw-r--r--services/core/java/com/android/server/display/utils/RollingBuffer.java184
-rw-r--r--services/core/java/com/android/server/display/whitebalance/AmbientFilter.java256
-rw-r--r--services/core/java/com/android/server/display/whitebalance/AmbientSensor.java381
-rw-r--r--services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java385
-rw-r--r--services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java171
-rw-r--r--services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceSettings.java230
-rw-r--r--services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceThrottler.java227
13 files changed, 2170 insertions, 3 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 67b06525a7e4..9fb8386f975c 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3778,4 +3778,105 @@
<!-- Whether or not aware is enabled by default -->
<bool name="config_awareSettingAvailable">false</bool>
+
+ <!-- Display White-Balance -->
+
+ <!-- See AmbientSensor.AmbientBrightnessSensor.
+ The ambient brightness sensor rate (in milliseconds). Must be positive. -->
+ <integer name="config_displayWhiteBalanceBrightnessSensorRate">250</integer>
+
+ <!-- See AmbientFilter.
+ How long ambient brightness changes are kept and taken into consideration
+ (in milliseconds). Must be positive. -->
+ <integer name="config_displayWhiteBalanceBrightnessFilterHorizon">10000</integer>
+
+ <!-- See AmbientFilter.WeightedMovingAverageAmbientFilter.
+ Recent changes are prioritised by integrating their duration over y = x + intercept
+ (the higher it is, the less prioritised recent changes are). Must be a non-negative
+ number, or NaN to avoid this implementation. -->
+ <item name="config_displayWhiteBalanceBrightnessFilterIntercept" format="float" type="dimen">10.0</item>
+
+ <!-- See AmbientSensor.AmbientColorTemperatureSensor.
+ The ambient color temperature sensor name. -->
+ <string name="config_displayWhiteBalanceColorTemperatureSensorName">com.google.sensor.color</string>
+
+ <!-- See AmbientSensor.AmbientColorTemperatureSensor.
+ The ambient color temperature sensor rate (in milliseconds). Must be positive. -->
+ <integer name="config_displayWhiteBalanceColorTemperatureSensorRate">250</integer>
+
+ <!-- See AmbientFilter.
+ How long ambient color temperature changes are kept and taken into consideration
+ (in milliseconds). Must be positive. -->
+ <integer name="config_displayWhiteBalanceColorTemperatureFilterHorizon">10000</integer>
+
+ <!-- See AmbientFilter.WeightedMovingAverageAmbientFilter.
+ Recent changes are prioritised by integrating their duration over y = x + intercept
+ (the higher it is, the less prioritised recent changes are). Must be a non-negative
+ number, or NaN to avoid this implementation. -->
+ <item name="config_displayWhiteBalanceColorTemperatureFilterIntercept" format="float"
+ type="dimen">10.0</item>
+
+ <!-- See DisplayWhiteBalanceThrottler.
+ The debounce time (in milliseconds) for increasing the screen color temperature, throttled
+ if time > lastTime + debounce. Must be non-negative. -->
+ <integer name="config_displayWhiteBalanceIncreaseDebounce">5000</integer>
+
+ <!-- See DisplayWhiteBalanceThrottler.
+ The debounce time (in milliseconds) for decreasing the screen color tempearture, throttled
+ if time < lastTime - debounce. Must be non-negative. -->
+ <integer name="config_displayWhiteBalanceDecreaseDebounce">5000</integer>
+
+ <!-- See DisplayWhiteBalanceThrottler.
+ The ambient color temperature values used to determine the threshold as the corresponding
+ value in config_displayWhiteBalance{Increase,Decrease}Threholds. Must be non-empty, the
+ same length as config_displayWhiteBalance{Increase,Decrease}Thresholds, and contain
+ non-negative, strictly increasing numbers.
+
+ For example, if:
+
+ - baseThresolds = [0, 100, 1000];
+ - increaseThresholds = [0.1, 0.15, 0.2];
+ - decreaseThresholds = [0.1, 0.05, 0.0];
+
+ Then, given the ambient color temperature INCREASED from X to Y (so X < Y):
+ - If 0 <= Y < 100, we require Y > (1 + 0.1) * X = 1.1X;
+ - If 100 <= Y < 1000, we require Y > (1 + 0.15) * X = 1.15X;
+ - If 1000 <= Y, we require Y > (1 + 0.2) * X = 1.2X.
+
+ Or, if the ambient color temperature DECREASED from X to Y (so X > Y):
+ - If 0 <= Y < 100, we require Y < (1 - 0.1) * X = 0.9X;
+ - If 100 <= Y < 1000, we require Y < (1 - 0.05) * X = 0.95X;
+ - If 1000 <= Y, we require Y < (1 - 0) * X = X.
+
+ NOTE: the numbers in this example are made up, and don't represent how actual base,
+ increase or decrease thresholds would look like. -->
+ <array name="config_displayWhiteBalanceBaseThresholds">
+ <item>0.0</item>
+ </array>
+
+ <!-- See DisplayWhiteBalanceThrottler.
+ The increase threshold values, throttled if value < value * (1 + threshold). Must be
+ non-empty, the same length as config_displayWhiteBalanceBaseThresholds, and contain
+ non-negative numbers. -->
+ <array name="config_displayWhiteBalanceIncreaseThresholds">
+ <item>0.1</item>
+ </array>
+
+ <!-- See DisplayWhiteBalanceThrottler.
+ The decrease threshold values, throttled if value > value * (1 - threshold). Must be
+ non-empty, the same length as config_displayWhiteBalanceBaseThresholds, and contain
+ non-negative numbers. -->
+ <array name="config_displayWhiteBalanceDecreaseThresholds">
+ <item>0.1</item>
+ </array>
+
+ <!-- See DisplayWhiteBalanceController.
+ The ambient brightness threshold (in lux) beneath which we fall back to a fixed ambient
+ color temperature. -->
+ <item name="config_displayWhiteBalanceLowLightAmbientBrightnessThreshold" format="float" type="dimen">10.0</item>
+
+ <!-- See DisplayWhiteBalanceController.
+ The ambient color temperature (in cct) to which we fall back when the ambient brightness
+ drops beneath a certain threshold. -->
+ <item name="config_displayWhiteBalanceLowLightAmbientColorTemperature" format="float" type="dimen">6500.0</item>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 54a3243dd562..1a9b6ab48f6d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3587,4 +3587,20 @@
<java-symbol type="integer" name="config_attentionApiTimeout" />
<java-symbol type="string" name="config_incidentReportApproverPackage" />
+
+ <!-- Display White-Balance -->
+ <java-symbol type="integer" name="config_displayWhiteBalanceBrightnessSensorRate" />
+ <java-symbol type="integer" name="config_displayWhiteBalanceBrightnessFilterHorizon" />
+ <java-symbol type="dimen" name="config_displayWhiteBalanceBrightnessFilterIntercept" />
+ <java-symbol type="string" name="config_displayWhiteBalanceColorTemperatureSensorName" />
+ <java-symbol type="integer" name="config_displayWhiteBalanceColorTemperatureSensorRate" />
+ <java-symbol type="integer" name="config_displayWhiteBalanceColorTemperatureFilterHorizon" />
+ <java-symbol type="dimen" name="config_displayWhiteBalanceColorTemperatureFilterIntercept" />
+ <java-symbol type="integer" name="config_displayWhiteBalanceIncreaseDebounce" />
+ <java-symbol type="integer" name="config_displayWhiteBalanceDecreaseDebounce" />
+ <java-symbol type="array" name="config_displayWhiteBalanceBaseThresholds" />
+ <java-symbol type="array" name="config_displayWhiteBalanceIncreaseThresholds" />
+ <java-symbol type="array" name="config_displayWhiteBalanceDecreaseThresholds" />
+ <java-symbol type="dimen" name="config_displayWhiteBalanceLowLightAmbientBrightnessThreshold" />
+ <java-symbol type="dimen" name="config_displayWhiteBalanceLowLightAmbientColorTemperature" />
</resources>
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index c9df86e83091..80ea1dae3115 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -2196,6 +2196,22 @@ public final class DisplayManagerService extends SystemService {
}
}
+ void setDisplayWhiteBalanceLoggingEnabled(boolean enabled) {
+ if (mDisplayPowerController != null) {
+ synchronized (mSyncRoot) {
+ mDisplayPowerController.setDisplayWhiteBalanceLoggingEnabled(enabled);
+ }
+ }
+ }
+
+ void setAmbientColorTemperatureOverride(float cct) {
+ if (mDisplayPowerController != null) {
+ synchronized (mSyncRoot) {
+ mDisplayPowerController.setAmbientColorTemperatureOverride(cct);
+ }
+ }
+ }
+
private boolean validatePackageName(int uid, String packageName) {
if (packageName != null) {
String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
index abbfc7b18f94..04d28eaa7f63 100644
--- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
+++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
@@ -45,6 +45,12 @@ class DisplayManagerShellCommand extends ShellCommand {
return setAutoBrightnessLoggingEnabled(true);
case "ab-logging-disable":
return setAutoBrightnessLoggingEnabled(false);
+ case "dwb-logging-enable":
+ return setDisplayWhiteBalanceLoggingEnabled(true);
+ case "dwb-logging-disable":
+ return setDisplayWhiteBalanceLoggingEnabled(false);
+ case "dwb-set-cct":
+ return setAmbientColorTemperatureOverride();
default:
return handleDefaultCommands(cmd);
}
@@ -65,6 +71,12 @@ class DisplayManagerShellCommand extends ShellCommand {
pw.println(" Enable auto-brightness logging.");
pw.println(" ab-logging-disable");
pw.println(" Disable auto-brightness logging.");
+ pw.println(" dwb-logging-enable");
+ pw.println(" Enable display white-balance logging.");
+ pw.println(" dwb-logging-disable");
+ pw.println(" Disable display white-balance logging.");
+ pw.println(" dwb-set-cct CCT");
+ pw.println(" Sets the ambient color temperature override to CCT (use -1 to disable).");
pw.println();
Intent.printIntentArgsHelp(pw , "");
}
@@ -75,7 +87,7 @@ class DisplayManagerShellCommand extends ShellCommand {
getErrPrintWriter().println("Error: no brightness specified");
return 1;
}
- float brightness = -1;
+ float brightness = -1.0f;
try {
brightness = Float.parseFloat(brightnessText);
} catch (NumberFormatException e) {
@@ -84,7 +96,7 @@ class DisplayManagerShellCommand extends ShellCommand {
getErrPrintWriter().println("Error: brightness should be a number between 0 and 1");
return 1;
}
- mService.setBrightness((int) brightness * 255);
+ mService.setBrightness((int) (brightness * 255));
return 0;
}
@@ -97,4 +109,26 @@ class DisplayManagerShellCommand extends ShellCommand {
mService.setAutoBrightnessLoggingEnabled(enabled);
return 0;
}
+
+ private int setDisplayWhiteBalanceLoggingEnabled(boolean enabled) {
+ mService.setDisplayWhiteBalanceLoggingEnabled(enabled);
+ return 0;
+ }
+
+ private int setAmbientColorTemperatureOverride() {
+ String cctText = getNextArg();
+ if (cctText == null) {
+ getErrPrintWriter().println("Error: no cct specified");
+ return 1;
+ }
+ float cct;
+ try {
+ cct = Float.parseFloat(cctText);
+ } catch (NumberFormatException e) {
+ getErrPrintWriter().println("Error: cct should be a number");
+ return 1;
+ }
+ mService.setAmbientColorTemperatureOverride(cct);
+ return 0;
+ }
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index db3928ec5fde..196b85b35c3c 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -52,6 +52,9 @@ import android.view.Display;
import com.android.internal.app.IBatteryStats;
import com.android.server.LocalServices;
import com.android.server.am.BatteryStatsService;
+import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
+import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory;
+import com.android.server.display.whitebalance.DisplayWhiteBalanceSettings;
import com.android.server.policy.WindowManagerPolicy;
import java.io.PrintWriter;
@@ -78,7 +81,8 @@ import java.io.PrintWriter;
* For debugging, you can make the color fade and brightness animations run
* slower by changing the "animator duration scale" option in Development Settings.
*/
-final class DisplayPowerController implements AutomaticBrightnessController.Callbacks {
+final class DisplayPowerController implements AutomaticBrightnessController.Callbacks,
+ DisplayWhiteBalanceController.Callbacks {
private static final String TAG = "DisplayPowerController";
private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked";
private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
@@ -307,6 +311,12 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
// Whether or not to skip the initial brightness ramps into STATE_ON.
private final boolean mSkipScreenOnBrightnessRamp;
+ // Display white balance components.
+ @Nullable
+ private final DisplayWhiteBalanceSettings mDisplayWhiteBalanceSettings;
+ @Nullable
+ private final DisplayWhiteBalanceController mDisplayWhiteBalanceController;
+
// A record of state for skipping brightness ramps.
private int mSkipRampState = RAMP_STATE_SKIP_NONE;
@@ -504,6 +514,20 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
mPendingScreenBrightnessSetting = -1;
mTemporaryAutoBrightnessAdjustment = Float.NaN;
mPendingAutoBrightnessAdjustment = Float.NaN;
+
+ DisplayWhiteBalanceSettings displayWhiteBalanceSettings = null;
+ DisplayWhiteBalanceController displayWhiteBalanceController = null;
+ try {
+ displayWhiteBalanceSettings = new DisplayWhiteBalanceSettings(mContext, mHandler);
+ displayWhiteBalanceController = DisplayWhiteBalanceFactory.create(mHandler,
+ mSensorManager, resources);
+ displayWhiteBalanceSettings.setCallbacks(this);
+ displayWhiteBalanceController.setCallbacks(this);
+ } catch (Exception e) {
+ Slog.e(TAG, "failed to set up display white-balance: " + e);
+ }
+ mDisplayWhiteBalanceSettings = displayWhiteBalanceSettings;
+ mDisplayWhiteBalanceController = displayWhiteBalanceController;
}
/**
@@ -526,6 +550,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
public void onSwitchUser(@UserIdInt int newUserId) {
handleSettingsChange(true /* userSwitch */);
mBrightnessTracker.onSwitchUser(newUserId);
+ if (mDisplayWhiteBalanceSettings != null) {
+ mDisplayWhiteBalanceSettings.onSwitchUser();
+ }
}
public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(
@@ -985,6 +1012,16 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
+ // Update display white-balance.
+ if (mDisplayWhiteBalanceController != null) {
+ if (state == Display.STATE_ON && mDisplayWhiteBalanceSettings.isEnabled()) {
+ mDisplayWhiteBalanceController.setEnabled(true);
+ mDisplayWhiteBalanceController.updateScreenColorTemperature();
+ } else {
+ mDisplayWhiteBalanceController.setEnabled(false);
+ }
+ }
+
// Determine whether the display is ready for use in the newly requested state.
// Note that we do not wait for the brightness ramp animation to complete before
// reporting the display is ready because we only need to ensure the screen is in the
@@ -1699,6 +1736,12 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
pw.println();
mBrightnessTracker.dump(pw);
}
+
+ pw.println();
+ if (mDisplayWhiteBalanceController != null) {
+ mDisplayWhiteBalanceController.dump(pw);
+ mDisplayWhiteBalanceSettings.dump(pw);
+ }
}
private static String proximityToString(int state) {
@@ -1845,4 +1888,26 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
mAutomaticBrightnessController.setLoggingEnabled(enabled);
}
}
+
+ @Override // DisplayWhiteBalanceController.Callbacks
+ public void updateWhiteBalance() {
+ sendUpdatePowerState();
+ }
+
+ void setDisplayWhiteBalanceLoggingEnabled(boolean enabled) {
+ if (mDisplayWhiteBalanceController != null) {
+ mDisplayWhiteBalanceController.setLoggingEnabled(enabled);
+ mDisplayWhiteBalanceSettings.setLoggingEnabled(enabled);
+ }
+ }
+
+ void setAmbientColorTemperatureOverride(float cct) {
+ if (mDisplayWhiteBalanceController != null) {
+ mDisplayWhiteBalanceController.setAmbientColorTemperatureOverride(cct);
+ // The ambient color temperature override is only applied when the ambient color
+ // temperature changes or is updated, so it doesn't necessarily change the screen color
+ // temperature immediately. So, let's make it!
+ sendUpdatePowerState();
+ }
+ }
}
diff --git a/services/core/java/com/android/server/display/utils/History.java b/services/core/java/com/android/server/display/utils/History.java
new file mode 100644
index 000000000000..ed171b8f7408
--- /dev/null
+++ b/services/core/java/com/android/server/display/utils/History.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 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.utils;
+
+import java.time.Clock;
+
+/**
+ * A fixed-size buffer that keeps the most recent values and their times.
+ *
+ * This class is used for logging and debugging purposes only, so there's no way to retrieve the
+ * history other than toString(), and a non-monotonic clock is good enough.
+ */
+public class History {
+
+ private int mSize;
+ private int mCount;
+ private int mStart;
+ private int mEnd;
+
+ private long[] mTimes;
+ private float[] mValues;
+
+ private Clock mClock;
+
+ /**
+ * @param size
+ * The maximum number of values kept.
+ */
+ public History(int size) {
+ this(size, Clock.systemUTC());
+ }
+
+ /**
+ * @param size
+ * The maximum number of values kept.
+ * @param clock
+ * The clock used.
+ */
+ public History(int size, Clock clock) {
+ mSize = size;
+ mCount = 0;
+ mStart = 0;
+ mEnd = 0;
+ mTimes = new long[size];
+ mValues = new float[size];
+ mClock = clock;
+ }
+
+ /**
+ * Add a value.
+ *
+ * @param value
+ * The value.
+ */
+ public void add(float value) {
+ mTimes[mEnd] = mClock.millis();
+ mValues[mEnd] = value;
+ if (mCount < mSize) {
+ mCount++;
+ } else {
+ mStart = (mStart + 1) % mSize;
+ }
+ mEnd = (mEnd + 1) % mSize;
+ }
+
+ /**
+ * Convert the buffer to string.
+ *
+ * @return The buffer as string.
+ */
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("[");
+ for (int i = 0; i < mCount; i++) {
+ final int index = (mStart + i) % mSize;
+ final long time = mTimes[index];
+ final float value = mValues[index];
+ sb.append(value + " @ " + time);
+ if (i + 1 != mCount) {
+ sb.append(", ");
+ }
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+}
diff --git a/services/core/java/com/android/server/display/utils/RollingBuffer.java b/services/core/java/com/android/server/display/utils/RollingBuffer.java
new file mode 100644
index 000000000000..dd5b7ab2403d
--- /dev/null
+++ b/services/core/java/com/android/server/display/utils/RollingBuffer.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2019 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.utils;
+
+/**
+ * A buffer that supports adding new values and truncating old ones.
+ */
+public class RollingBuffer {
+
+ private static final int INITIAL_SIZE = 50;
+
+ private int mSize;
+ private int mCount;
+ private int mStart;
+ private int mEnd;
+
+ private long[] mTimes; // Milliseconds
+ private float[] mValues;
+
+ public RollingBuffer() {
+ mSize = INITIAL_SIZE;
+ mTimes = new long[INITIAL_SIZE];
+ mValues = new float[INITIAL_SIZE];
+ clear();
+ }
+
+ /**
+ * Add a value at a given time.
+ *
+ * @param time
+ * The time (in milliseconds).
+ * @param value
+ * The value.
+ */
+ public void add(long time, float value) {
+ if (mCount >= mSize) {
+ expandBuffer();
+ }
+ mTimes[mEnd] = time;
+ mValues[mEnd] = value;
+ mEnd = (mEnd + 1) % mSize;
+ mCount++;
+ }
+
+ /**
+ * Get the size of the buffer.
+ *
+ * @return The size of the buffer.
+ */
+ public int size() {
+ return mCount;
+ }
+
+ /**
+ * Return whether the buffer is empty or not.
+ *
+ * @return Whether the buffer is empty or not.
+ */
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
+ * Get a time.
+ *
+ * @param index
+ * The index of the time.
+ *
+ * @return The time.
+ */
+ public long getTime(int index) {
+ return mTimes[offsetOf(index)];
+ }
+
+ /**
+ * Get a value.
+ *
+ * @param index
+ * The index of the value.
+ *
+ * @return The value.
+ */
+ public float getValue(int index) {
+ return mValues[offsetOf(index)];
+ }
+
+ /**
+ * Truncate old values.
+ *
+ * @param minTime
+ * The minimum time (all values older than this time are truncated).
+ */
+ public void truncate(long minTime) {
+ if (isEmpty() || getTime(0) >= minTime) {
+ return;
+ }
+ final int index = getLatestIndexBefore(minTime);
+ mStart = offsetOf(index);
+ mCount -= index;
+ // Remove everything that happened before mTimes[index], but set the index-th value time to
+ // minTime rather than dropping it, as that would've been the value between the minTime and
+ // mTimes[index+1].
+ //
+ // -*---*---|---*---*- => xxxxxxxxx|*--*---*- rather than xxxxxxxxx|???*---*-
+ // ^ ^ ^ ^ ^
+ // i i+1 i i+1 i+1
+ mTimes[mStart] = minTime;
+ }
+
+ /**
+ * Clears the buffer.
+ */
+ public void clear() {
+ mCount = 0;
+ mStart = 0;
+ mEnd = 0;
+ }
+
+ /**
+ * Convert the buffer to string.
+ *
+ * @return The buffer as string.
+ */
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("[");
+ for (int i = 0; i < mCount; i++) {
+ final int index = offsetOf(i);
+ sb.append(mValues[index] + " @ " + mTimes[index]);
+ if (i + 1 != mCount) {
+ sb.append(", ");
+ }
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+ private int offsetOf(int index) {
+ if (index < 0 || index >= mCount) {
+ throw new ArrayIndexOutOfBoundsException("invalid index: " + index + ", mCount= "
+ + mCount);
+ }
+ return (mStart + index) % mSize;
+ }
+
+ private void expandBuffer() {
+ final int size = mSize * 2;
+ long[] times = new long[size];
+ float[] values = new float[size];
+ System.arraycopy(mTimes, mStart, times, 0, mCount - mStart);
+ System.arraycopy(mTimes, 0, times, mCount - mStart, mStart);
+ System.arraycopy(mValues, mStart, values, 0, mCount - mStart);
+ System.arraycopy(mValues, 0, values, mCount - mStart, mStart);
+ mSize = size;
+ mStart = 0;
+ mEnd = mCount;
+ mTimes = times;
+ mValues = values;
+ }
+
+ private int getLatestIndexBefore(long time) {
+ for (int i = 1; i < mCount; i++) {
+ if (mTimes[offsetOf(i)] > time) {
+ return i - 1;
+ }
+ }
+ return mCount - 1;
+ }
+
+}
diff --git a/services/core/java/com/android/server/display/whitebalance/AmbientFilter.java b/services/core/java/com/android/server/display/whitebalance/AmbientFilter.java
new file mode 100644
index 000000000000..532bbed9588e
--- /dev/null
+++ b/services/core/java/com/android/server/display/whitebalance/AmbientFilter.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2019 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.whitebalance;
+
+import android.util.Slog;
+
+import com.android.server.display.utils.RollingBuffer;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+/**
+ * The DisplayWhiteBalanceController uses the AmbientFilter to average ambient changes over time,
+ * filter out the noise, and arrive at an estimate of the actual value.
+ *
+ * When the DisplayWhiteBalanceController detects a change in ambient brightness or color
+ * temperature, it passes it to the AmbientFilter, and when it needs the actual ambient value, it
+ * asks it for an estimate.
+ *
+ * Implementations:
+ * - {@link WeightedMovingAverageAmbientFilter}
+ * A weighted average prioritising recent changes.
+ */
+abstract class AmbientFilter {
+
+ protected static final boolean DEBUG = false; // Enable for verbose logs.
+
+ protected final String mTag;
+ protected boolean mLoggingEnabled;
+
+ // How long ambient value changes are kept and taken into consideration.
+ private final int mHorizon; // Milliseconds
+
+ private final RollingBuffer mBuffer;
+
+ /**
+ * @param tag
+ * The tag used for dumping and logging.
+ * @param horizon
+ * How long ambient value changes are kept and taken into consideration.
+ *
+ * @throws IllegalArgumentException
+ * - horizon is not positive.
+ */
+ AmbientFilter(String tag, int horizon) {
+ validateArguments(horizon);
+ mTag = tag;
+ mLoggingEnabled = false;
+ mHorizon = horizon;
+ mBuffer = new RollingBuffer();
+ }
+
+ /**
+ * Add an ambient value change.
+ *
+ * @param time
+ * The time.
+ * @param value
+ * The ambient value.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean addValue(long time, float value) {
+ if (value < 0.0f) {
+ return false;
+ }
+ truncateOldValues(time);
+ if (mLoggingEnabled) {
+ Slog.d(mTag, "add value: " + value + " @ " + time);
+ }
+ mBuffer.add(time, value);
+ return true;
+ }
+
+ /**
+ * Get an estimate of the actual ambient color temperature.
+ *
+ * @param time
+ * The time.
+ *
+ * @return An estimate of the actual ambient color temperature.
+ */
+ public float getEstimate(long time) {
+ truncateOldValues(time);
+ final float value = filter(time, mBuffer);
+ if (mLoggingEnabled) {
+ Slog.d(mTag, "get estimate: " + value + " @ " + time);
+ }
+ return value;
+ }
+
+ /**
+ * Clears the filter state.
+ */
+ public void clear() {
+ mBuffer.clear();
+ }
+
+ /**
+ * Enable/disable logging.
+ *
+ * @param loggingEnabled
+ * Whether logging is on/off.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setLoggingEnabled(boolean loggingEnabled) {
+ if (mLoggingEnabled == loggingEnabled) {
+ return false;
+ }
+ mLoggingEnabled = loggingEnabled;
+ return true;
+ }
+
+ /**
+ * Dump the state.
+ *
+ * @param writer
+ * The PrintWriter used to dump the state.
+ */
+ public void dump(PrintWriter writer) {
+ writer.println(" " + mTag);
+ writer.println(" mLoggingEnabled=" + mLoggingEnabled);
+ writer.println(" mHorizon=" + mHorizon);
+ writer.println(" mBuffer=" + mBuffer);
+ }
+
+ private void validateArguments(int horizon) {
+ if (horizon <= 0) {
+ throw new IllegalArgumentException("horizon must be positive");
+ }
+ }
+
+ private void truncateOldValues(long time) {
+ final long minTime = time - mHorizon;
+ mBuffer.truncate(minTime);
+ }
+
+ protected abstract float filter(long time, RollingBuffer buffer);
+
+ /**
+ * A weighted average prioritising recent changes.
+ */
+ static class WeightedMovingAverageAmbientFilter extends AmbientFilter {
+
+ // How long the latest ambient value change is predicted to last.
+ private static final int PREDICTION_TIME = 100; // Milliseconds
+
+ // Recent changes are prioritised by integrating their duration over y = x + mIntercept
+ // (the higher it is, the less prioritised recent changes are).
+ private final float mIntercept;
+
+ /**
+ * @param tag
+ * The tag used for dumping and logging.
+ * @param horizon
+ * How long ambient value changes are kept and taken into consideration.
+ * @param intercept
+ * Recent changes are prioritised by integrating their duration over y = x + intercept
+ * (the higher it is, the less prioritised recent changes are).
+ *
+ * @throws IllegalArgumentException
+ * - horizon is not positive.
+ * - intercept is NaN or negative.
+ */
+ WeightedMovingAverageAmbientFilter(String tag, int horizon, float intercept) {
+ super(tag, horizon);
+ validateArguments(intercept);
+ mIntercept = intercept;
+ }
+
+ /**
+ * See {@link AmbientFilter#dump base class}.
+ */
+ @Override
+ public void dump(PrintWriter writer) {
+ super.dump(writer);
+ writer.println(" mIntercept=" + mIntercept);
+ }
+
+ // Normalise the times to [t1=0, t2, ..., tN, now + PREDICTION_TIME], so the first change
+ // starts at 0 and the last change is predicted to last a bit, and divide them by 1000 as
+ // milliseconds are high enough to overflow.
+ // The weight of the value from t[i] to t[i+1] is the area under (A.K.A. the integral of)
+ // y = x + mIntercept from t[i] to t[i+1].
+ @Override
+ protected float filter(long time, RollingBuffer buffer) {
+ if (buffer.isEmpty()) {
+ return -1.0f;
+ }
+ float total = 0.0f;
+ float totalWeight = 0.0f;
+ final float[] weights = getWeights(time, buffer);
+ if (DEBUG && mLoggingEnabled) {
+ Slog.v(mTag, "filter: " + buffer + " => " + Arrays.toString(weights));
+ }
+ for (int i = 0; i < weights.length; i++) {
+ final float value = buffer.getValue(i);
+ final float weight = weights[i];
+ total += weight * value;
+ totalWeight += weight;
+ }
+ if (totalWeight == 0.0f) {
+ return buffer.getValue(buffer.size() - 1);
+ }
+ return total / totalWeight;
+ }
+
+ private void validateArguments(float intercept) {
+ if (Float.isNaN(intercept) || intercept < 0.0f) {
+ throw new IllegalArgumentException("intercept must be a non-negative number");
+ }
+ }
+
+ private float[] getWeights(long time, RollingBuffer buffer) {
+ float[] weights = new float[buffer.size()];
+ final long startTime = buffer.getTime(0);
+ float previousTime = 0.0f;
+ for (int i = 1; i < weights.length; i++) {
+ final float currentTime = (buffer.getTime(i) - startTime) / 1000.0f;
+ final float weight = calculateIntegral(previousTime, currentTime);
+ weights[i - 1] = weight;
+ previousTime = currentTime;
+ }
+ final float lastTime = (time + PREDICTION_TIME - startTime) / 1000.0f;
+ final float lastWeight = calculateIntegral(previousTime, lastTime);
+ weights[weights.length - 1] = lastWeight;
+ return weights;
+ }
+
+ private float calculateIntegral(float from, float to) {
+ return antiderivative(to) - antiderivative(from);
+ }
+
+ private float antiderivative(float x) {
+ // f(x) = x + c => F(x) = 1/2 * x^2 + c * x
+ return 0.5f * x * x + mIntercept * x;
+ }
+
+ }
+
+}
diff --git a/services/core/java/com/android/server/display/whitebalance/AmbientSensor.java b/services/core/java/com/android/server/display/whitebalance/AmbientSensor.java
new file mode 100644
index 000000000000..1707a62f805a
--- /dev/null
+++ b/services/core/java/com/android/server/display/whitebalance/AmbientSensor.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2019 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.whitebalance;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.util.Slog;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.display.utils.History;
+
+import java.io.PrintWriter;
+
+/**
+ * The DisplayWhiteBalanceController uses the AmbientSensor to detect changes in the ambient
+ * brightness and color temperature.
+ *
+ * The AmbientSensor listens on an actual sensor, derives the ambient brightness or color
+ * temperature from its events, and calls back into the DisplayWhiteBalanceController to report it.
+ */
+abstract class AmbientSensor {
+
+ protected String mTag;
+ protected boolean mLoggingEnabled;
+
+ private final Handler mHandler;
+
+ protected final SensorManager mSensorManager;
+ protected Sensor mSensor;
+
+ private boolean mEnabled;
+
+ private int mRate; // Milliseconds
+
+ // The total events count and the most recent events are kept for debugging purposes.
+ private int mEventsCount;
+ private static final int HISTORY_SIZE = 50;
+ private History mEventsHistory;
+
+ /**
+ * @param tag
+ * The tag used for dumping and logging.
+ * @param handler
+ * The handler used to determine which thread to run on.
+ * @param sensorManager
+ * The sensor manager used to acquire necessary sensors.
+ * @param rate
+ * The sensor rate.
+ *
+ * @throws IllegalArgumentException
+ * - rate is not positive.
+ * @throws NullPointerException
+ * - handler is null;
+ * - sensorManager is null.
+ * @throws IllegalStateException
+ * - Cannot find the necessary sensor.
+ */
+ AmbientSensor(String tag, @NonNull Handler handler, @NonNull SensorManager sensorManager,
+ int rate) {
+ validateArguments(handler, sensorManager, rate);
+ mTag = tag;
+ mLoggingEnabled = false;
+ mHandler = handler;
+ mSensorManager = sensorManager;
+ mEnabled = false;
+ mRate = rate;
+ mEventsCount = 0;
+ mEventsHistory = new History(HISTORY_SIZE);
+ }
+
+ /**
+ * Enable/disable the sensor.
+ *
+ * @param enabled
+ * Whether the sensor should be on/off.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setEnabled(boolean enabled) {
+ if (enabled) {
+ return enable();
+ } else {
+ return disable();
+ }
+ }
+
+ /**
+ * Enable/disable logging.
+ *
+ * @param loggingEnabled
+ * Whether logging should be on/off.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setLoggingEnabled(boolean loggingEnabled) {
+ if (mLoggingEnabled == loggingEnabled) {
+ return false;
+ }
+ mLoggingEnabled = loggingEnabled;
+ return true;
+ }
+
+ /**
+ * Dump the state.
+ *
+ * @param writer
+ * The PrintWriter used to dump the state.
+ */
+ public void dump(PrintWriter writer) {
+ writer.println(" " + mTag);
+ writer.println(" mLoggingEnabled=" + mLoggingEnabled);
+ writer.println(" mHandler=" + mHandler);
+ writer.println(" mSensorManager=" + mSensorManager);
+ writer.println(" mSensor=" + mSensor);
+ writer.println(" mEnabled=" + mEnabled);
+ writer.println(" mRate=" + mRate);
+ writer.println(" mEventsCount=" + mEventsCount);
+ writer.println(" mEventsHistory=" + mEventsHistory);
+ }
+
+
+ private static void validateArguments(Handler handler, SensorManager sensorManager, int rate) {
+ Preconditions.checkNotNull(handler, "handler cannot be null");
+ Preconditions.checkNotNull(sensorManager, "sensorManager cannot be null");
+ if (rate <= 0) {
+ throw new IllegalArgumentException("rate must be positive");
+ }
+ }
+
+ protected abstract void update(float value);
+
+ private boolean enable() {
+ if (mEnabled) {
+ return false;
+ }
+ if (mLoggingEnabled) {
+ Slog.d(mTag, "enabling");
+ }
+ mEnabled = true;
+ startListening();
+ return true;
+ }
+
+ private boolean disable() {
+ if (!mEnabled) {
+ return false;
+ }
+ if (mLoggingEnabled) {
+ Slog.d(mTag, "disabling");
+ }
+ mEnabled = false;
+ mEventsCount = 0;
+ stopListening();
+ return true;
+ }
+
+ private void startListening() {
+ if (mSensorManager == null) {
+ return;
+ }
+ mSensorManager.registerListener(mListener, mSensor, mRate * 1000, mHandler);
+ }
+
+ private void stopListening() {
+ if (mSensorManager == null) {
+ return;
+ }
+ mSensorManager.unregisterListener(mListener);
+ }
+
+ private void handleNewEvent(float value) {
+ // This shouldn't really happen, except for the race condition where the sensor is disabled
+ // with an event already in the handler queue, in which case we discard that event.
+ if (!mEnabled) {
+ return;
+ }
+ if (mLoggingEnabled) {
+ Slog.d(mTag, "handle new event: " + value);
+ }
+ mEventsCount++;
+ mEventsHistory.add(value);
+ update(value);
+ }
+
+ private SensorEventListener mListener = new SensorEventListener() {
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ final float value = event.values[0];
+ handleNewEvent(value);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ // Not used.
+ }
+
+ };
+
+ /**
+ * A sensor that reports the ambient brightness.
+ */
+ static class AmbientBrightnessSensor extends AmbientSensor {
+
+ private static final String TAG = "AmbientBrightnessSensor";
+
+ // To decouple the DisplayWhiteBalanceController from the AmbientBrightnessSensor, the
+ // DWBC implements Callbacks and passes itself to the ABS so it can call back into it
+ // without knowing about it.
+ @Nullable
+ private Callbacks mCallbacks;
+
+ /**
+ * @param handler
+ * The handler used to determine which thread to run on.
+ * @param sensorManager
+ * The sensor manager used to acquire necessary sensors.
+ * @param rate
+ * The sensor rate.
+ *
+ * @throws IllegalArgumentException
+ * - rate is not positive.
+ * @throws NullPointerException
+ * - handler is null;
+ * - sensorManager is null.
+ * @throws IllegalStateException
+ * - Cannot find the light sensor.
+ */
+ AmbientBrightnessSensor(@NonNull Handler handler, @NonNull SensorManager sensorManager,
+ int rate) {
+ super(TAG, handler, sensorManager, rate);
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
+ if (mSensor == null) {
+ throw new IllegalStateException("cannot find light sensor");
+ }
+ mCallbacks = null;
+ }
+
+ /**
+ * Set an object to call back to when the ambient brightness changes.
+ *
+ * @param callbacks
+ * The object to call back to.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setCallbacks(Callbacks callbacks) {
+ if (mCallbacks == callbacks) {
+ return false;
+ }
+ mCallbacks = callbacks;
+ return true;
+ }
+
+ /**
+ * See {@link AmbientSensor#dump base class}.
+ */
+ @Override
+ public void dump(PrintWriter writer) {
+ super.dump(writer);
+ writer.println(" mCallbacks=" + mCallbacks);
+ }
+
+ interface Callbacks {
+ void onAmbientBrightnessChanged(float value);
+ }
+
+ @Override
+ protected void update(float value) {
+ if (mCallbacks != null) {
+ mCallbacks.onAmbientBrightnessChanged(value);
+ }
+ }
+
+ }
+
+ /**
+ * A sensor that reports the ambient color temperature.
+ */
+ static class AmbientColorTemperatureSensor extends AmbientSensor {
+
+ private static final String TAG = "AmbientColorTemperatureSensor";
+
+ // To decouple the DisplayWhiteBalanceController from the
+ // AmbientColorTemperatureSensor, the DWBC implements Callbacks and passes itself to the
+ // ACTS so it can call back into it without knowing about it.
+ @Nullable
+ private Callbacks mCallbacks;
+
+ /**
+ * @param handler
+ * The handler used to determine which thread to run on.
+ * @param sensorManager
+ * The sensor manager used to acquire necessary sensors.
+ * @param name
+ * The color sensor name.
+ * @param rate
+ * The sensor rate.
+ *
+ * @throws IllegalArgumentException
+ * - rate is not positive.
+ * @throws NullPointerException
+ * - handler is null;
+ * - sensorManager is null.
+ * @throws IllegalStateException
+ * - Cannot find the color sensor.
+ */
+ AmbientColorTemperatureSensor(@NonNull Handler handler,
+ @NonNull SensorManager sensorManager, String name, int rate) {
+ super(TAG, handler, sensorManager, rate);
+ mSensor = null;
+ for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_ALL)) {
+ if (sensor.getStringType().equals(name)) {
+ mSensor = sensor;
+ break;
+ }
+ }
+ if (mSensor == null) {
+ throw new IllegalStateException("cannot find sensor " + name);
+ }
+ mCallbacks = null;
+ }
+
+ /**
+ * Set an object to call back to when the ambient color temperature changes.
+ *
+ * @param callbacks
+ * The object to call back to.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setCallbacks(Callbacks callbacks) {
+ if (mCallbacks == callbacks) {
+ return false;
+ }
+ mCallbacks = callbacks;
+ return true;
+ }
+
+ /**
+ * See {@link AmbientSensor#dump base class}.
+ */
+ @Override
+ public void dump(PrintWriter writer) {
+ super.dump(writer);
+ writer.println(" mCallbacks=" + mCallbacks);
+ }
+
+ interface Callbacks {
+ void onAmbientColorTemperatureChanged(float value);
+ }
+
+ @Override
+ protected void update(float value) {
+ if (mCallbacks != null) {
+ mCallbacks.onAmbientColorTemperatureChanged(value);
+ }
+ }
+
+ }
+
+}
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
new file mode 100644
index 000000000000..7ae00af626c8
--- /dev/null
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2019 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.whitebalance;
+
+import android.annotation.NonNull;
+import android.util.Slog;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.LocalServices;
+import com.android.server.display.ColorDisplayService.ColorDisplayServiceInternal;
+import com.android.server.display.utils.History;
+
+import java.io.PrintWriter;
+
+/**
+ * The DisplayWhiteBalanceController drives display white-balance (automatically correcting the
+ * screen color temperature depending on the ambient color temperature).
+ *
+ * The DisplayWhiteBalanceController:
+ * - Uses the AmbientColorTemperatureSensor to detect changes in the ambient color temperature;
+ * - Uses the AmbientColorTemperatureFilter to average these changes over time, filter out the
+ * noise, and arrive at an estimate of the actual ambient color temperature;
+ * - Uses the DisplayWhiteBalanceThrottler to decide whether the screen color tempearture should be
+ * updated, suppressing changes that are too frequent or too minor.
+ */
+public class DisplayWhiteBalanceController implements
+ AmbientSensor.AmbientBrightnessSensor.Callbacks,
+ AmbientSensor.AmbientColorTemperatureSensor.Callbacks {
+
+ protected static final String TAG = "DisplayWhiteBalanceController";
+ protected boolean mLoggingEnabled;
+
+ private boolean mEnabled;
+
+ // To decouple the DisplayPowerController from the DisplayWhiteBalanceController, the DPC
+ // implements Callbacks and passes itself to the DWBC so it can call back into it without
+ // knowing about it.
+ private Callbacks mCallbacks;
+
+ private AmbientSensor.AmbientBrightnessSensor mBrightnessSensor;
+ private AmbientFilter mBrightnessFilter;
+ private AmbientSensor.AmbientColorTemperatureSensor mColorTemperatureSensor;
+ private AmbientFilter mColorTemperatureFilter;
+ private DisplayWhiteBalanceThrottler mThrottler;
+
+ // When the brightness drops below a certain threshold, it affects the color temperature
+ // accuracy, so we fall back to a fixed ambient color temperature.
+ private final float mLowLightAmbientBrightnessThreshold;
+ private final float mLowLightAmbientColorTemperature;
+
+ private float mAmbientColorTemperature;
+ private float mPendingAmbientColorTemperature;
+ private float mLastAmbientColorTemperature;
+
+ private ColorDisplayServiceInternal mColorDisplayServiceInternal;
+
+ // The most recent ambient color temperature values are kept for debugging purposes.
+ private static final int HISTORY_SIZE = 50;
+ private History mAmbientColorTemperatureHistory;
+
+ // Override the ambient color temperature for debugging purposes.
+ private float mAmbientColorTemperatureOverride;
+
+ /**
+ * @param brightnessSensor
+ * The sensor used to detect changes in the ambient brightness.
+ * @param brightnessFilter
+ * The filter used to avergae ambient brightness changes over time, filter out the noise
+ * and arrive at an estimate of the actual ambient brightness.
+ * @param colorTemperatureSensor
+ * The sensor used to detect changes in the ambient color temperature.
+ * @param colorTemperatureFilter
+ * The filter used to average ambient color temperature changes over time, filter out the
+ * noise and arrive at an estimate of the actual ambient color temperature.
+ * @param throttler
+ * The throttler used to determine whether the new screen color temperature should be
+ * updated or not.
+ * @param lowLightAmbientBrightnessThreshold
+ * The ambient brightness threshold beneath which we fall back to a fixed ambient color
+ * temperature.
+ * @param lowLightAmbientColorTemperature
+ * The ambient color temperature to which we fall back when the ambient brightness drops
+ * beneath a certain threshold.
+ *
+ * @throws NullPointerException
+ * - brightnessSensor is null;
+ * - brightnessFilter is null;
+ * - colorTemperatureSensor is null;
+ * - colorTemperatureFilter is null;
+ * - throttler is null.
+ */
+ public DisplayWhiteBalanceController(
+ @NonNull AmbientSensor.AmbientBrightnessSensor brightnessSensor,
+ @NonNull AmbientFilter brightnessFilter,
+ @NonNull AmbientSensor.AmbientColorTemperatureSensor colorTemperatureSensor,
+ @NonNull AmbientFilter colorTemperatureFilter,
+ @NonNull DisplayWhiteBalanceThrottler throttler,
+ float lowLightAmbientBrightnessThreshold, float lowLightAmbientColorTemperature) {
+ validateArguments(brightnessSensor, brightnessFilter, colorTemperatureSensor,
+ colorTemperatureFilter, throttler);
+ mLoggingEnabled = false;
+ mEnabled = false;
+ mCallbacks = null;
+ mBrightnessSensor = brightnessSensor;
+ mBrightnessFilter = brightnessFilter;
+ mColorTemperatureSensor = colorTemperatureSensor;
+ mColorTemperatureFilter = colorTemperatureFilter;
+ mThrottler = throttler;
+ mLowLightAmbientBrightnessThreshold = lowLightAmbientBrightnessThreshold;
+ mLowLightAmbientColorTemperature = lowLightAmbientColorTemperature;
+ mAmbientColorTemperature = -1.0f;
+ mPendingAmbientColorTemperature = -1.0f;
+ mLastAmbientColorTemperature = -1.0f;
+ mAmbientColorTemperatureHistory = new History(HISTORY_SIZE);
+ mAmbientColorTemperatureOverride = -1.0f;
+ mColorDisplayServiceInternal = LocalServices.getService(ColorDisplayServiceInternal.class);
+ }
+
+ /**
+ * Enable/disable the controller.
+ *
+ * @param enabled
+ * Whether the controller should be on/off.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setEnabled(boolean enabled) {
+ if (enabled) {
+ return enable();
+ } else {
+ return disable();
+ }
+ }
+
+ /**
+ * Set an object to call back to when the screen color temperature should be updated.
+ *
+ * @param callbacks
+ * The object to call back to.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setCallbacks(Callbacks callbacks) {
+ if (mCallbacks == callbacks) {
+ return false;
+ }
+ mCallbacks = callbacks;
+ return true;
+ }
+
+ /**
+ * Enable/disable logging.
+ *
+ * @param loggingEnabled
+ * Whether logging should be on/off.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setLoggingEnabled(boolean loggingEnabled) {
+ if (mLoggingEnabled == loggingEnabled) {
+ return false;
+ }
+ mLoggingEnabled = loggingEnabled;
+ mBrightnessSensor.setLoggingEnabled(loggingEnabled);
+ mBrightnessFilter.setLoggingEnabled(loggingEnabled);
+ mColorTemperatureSensor.setLoggingEnabled(loggingEnabled);
+ mColorTemperatureFilter.setLoggingEnabled(loggingEnabled);
+ mThrottler.setLoggingEnabled(loggingEnabled);
+ return true;
+ }
+
+ /**
+ * Set the ambient color temperature override.
+ *
+ * This is only applied when the ambient color temperature changes or is updated (in which case
+ * it overrides the ambient color temperature estimate); in other words, it doesn't necessarily
+ * change the screen color temperature immediately.
+ *
+ * @param ambientColorTemperatureOverride
+ * The ambient color temperature override.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setAmbientColorTemperatureOverride(float ambientColorTemperatureOverride) {
+ if (mAmbientColorTemperatureOverride == ambientColorTemperatureOverride) {
+ return false;
+ }
+ mAmbientColorTemperatureOverride = ambientColorTemperatureOverride;
+ return true;
+ }
+
+ /**
+ * Dump the state.
+ *
+ * @param writer
+ * The writer used to dump the state.
+ */
+ public void dump(PrintWriter writer) {
+ writer.println("DisplayWhiteBalanceController");
+ writer.println(" mLoggingEnabled=" + mLoggingEnabled);
+ writer.println(" mEnabled=" + mEnabled);
+ writer.println(" mCallbacks=" + mCallbacks);
+ mBrightnessSensor.dump(writer);
+ mBrightnessFilter.dump(writer);
+ mColorTemperatureSensor.dump(writer);
+ mColorTemperatureFilter.dump(writer);
+ mThrottler.dump(writer);
+ writer.println(" mLowLightAmbientBrightnessThreshold="
+ + mLowLightAmbientBrightnessThreshold);
+ writer.println(" mLowLightAmbientColorTemperature=" + mLowLightAmbientColorTemperature);
+ writer.println(" mAmbientColorTemperature=" + mAmbientColorTemperature);
+ writer.println(" mPendingAmbientColorTemperature=" + mPendingAmbientColorTemperature);
+ writer.println(" mLastAmbientColorTemperature=" + mLastAmbientColorTemperature);
+ writer.println(" mAmbientColorTemperatureHistory=" + mAmbientColorTemperatureHistory);
+ writer.println(" mAmbientColorTemperatureOverride=" + mAmbientColorTemperatureOverride);
+ }
+
+ @Override // AmbientSensor.AmbientBrightnessSensor.Callbacks
+ public void onAmbientBrightnessChanged(float value) {
+ final long time = System.currentTimeMillis();
+ mBrightnessFilter.addValue(time, value);
+ updateAmbientColorTemperature();
+ }
+
+ @Override // AmbientSensor.AmbientColorTemperatureSensor.Callbacks
+ public void onAmbientColorTemperatureChanged(float value) {
+ final long time = System.currentTimeMillis();
+ mColorTemperatureFilter.addValue(time, value);
+ updateAmbientColorTemperature();
+ }
+
+ /**
+ * Updates the ambient color temperature.
+ */
+ public void updateAmbientColorTemperature() {
+ final long time = System.currentTimeMillis();
+ float ambientColorTemperature = mColorTemperatureFilter.getEstimate(time);
+
+ final float ambientBrightness = mBrightnessFilter.getEstimate(time);
+ if (ambientBrightness < mLowLightAmbientBrightnessThreshold) {
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "low light ambient brightness: " + ambientBrightness + " < "
+ + mLowLightAmbientBrightnessThreshold
+ + ", falling back to fixed ambient color temperature: "
+ + ambientColorTemperature + " => " + mLowLightAmbientColorTemperature);
+ }
+ ambientColorTemperature = mLowLightAmbientColorTemperature;
+ }
+
+ if (mAmbientColorTemperatureOverride != -1.0f) {
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "override ambient color temperature: " + ambientColorTemperature
+ + " => " + mAmbientColorTemperatureOverride);
+ }
+ ambientColorTemperature = mAmbientColorTemperatureOverride;
+ }
+
+ // When the screen color temperature needs to be updated, we call DisplayPowerController to
+ // call our updateColorTemperature. The reason we don't call it directly is that we want
+ // all changes to the system to happen in a predictable order in DPC's main loop
+ // (updatePowerState).
+ if (ambientColorTemperature == -1.0f || mThrottler.throttle(ambientColorTemperature)) {
+ return;
+ }
+
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "pending ambient color temperature: " + ambientColorTemperature);
+ }
+ mPendingAmbientColorTemperature = ambientColorTemperature;
+ if (mCallbacks != null) {
+ mCallbacks.updateWhiteBalance();
+ }
+ }
+
+ /**
+ * Updates the screen color temperature.
+ */
+ public void updateScreenColorTemperature() {
+ float ambientColorTemperature = -1.0f;
+
+ // If both the pending and the current ambient color temperatures are -1, it means the DWBC
+ // was just enabled, and we use the last ambient color temperature until new sensor events
+ // give us a better estimate.
+ if (mAmbientColorTemperature == -1.0f && mPendingAmbientColorTemperature == -1.0f) {
+ ambientColorTemperature = mLastAmbientColorTemperature;
+ }
+
+ // Otherwise, we use the pending ambient color temperature, but only if it's non-trivial
+ // and different than the current one.
+ if (mPendingAmbientColorTemperature != -1.0f
+ && mPendingAmbientColorTemperature != mAmbientColorTemperature) {
+ ambientColorTemperature = mPendingAmbientColorTemperature;
+ }
+
+ if (ambientColorTemperature == -1.0f) {
+ return;
+ }
+
+ mAmbientColorTemperature = ambientColorTemperature;
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "ambient color temperature: " + mAmbientColorTemperature);
+ }
+ mPendingAmbientColorTemperature = -1.0f;
+ mAmbientColorTemperatureHistory.add(mAmbientColorTemperature);
+ mColorDisplayServiceInternal.setDisplayWhiteBalanceColorTemperature(
+ (int) mAmbientColorTemperature);
+ mLastAmbientColorTemperature = mAmbientColorTemperature;
+ }
+
+ /**
+ * The DisplayWhiteBalanceController decouples itself from its parent (DisplayPowerController)
+ * by providing this interface to implement (and a method to set its callbacks object), and
+ * calling these methods.
+ */
+ public interface Callbacks {
+
+ /**
+ * Called whenever the display white-balance state has changed.
+ *
+ * Usually, this means the estimated ambient color temperature has changed enough, and the
+ * screen color temperature should be updated; but it is also called by
+ */
+ void updateWhiteBalance();
+ }
+
+ private void validateArguments(AmbientSensor.AmbientBrightnessSensor brightnessSensor,
+ AmbientFilter brightnessFilter,
+ AmbientSensor.AmbientColorTemperatureSensor colorTemperatureSensor,
+ AmbientFilter colorTemperatureFilter,
+ DisplayWhiteBalanceThrottler throttler) {
+ Preconditions.checkNotNull(brightnessSensor, "brightnessSensor must not be null");
+ Preconditions.checkNotNull(brightnessFilter, "brightnessFilter must not be null");
+ Preconditions.checkNotNull(colorTemperatureSensor,
+ "colorTemperatureSensor must not be null");
+ Preconditions.checkNotNull(colorTemperatureFilter,
+ "colorTemperatureFilter must not be null");
+ Preconditions.checkNotNull(throttler, "throttler cannot be null");
+ }
+
+ private boolean enable() {
+ if (mEnabled) {
+ return false;
+ }
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "enabling");
+ }
+ mEnabled = true;
+ mBrightnessSensor.setEnabled(true);
+ mColorTemperatureSensor.setEnabled(true);
+ return true;
+ }
+
+ private boolean disable() {
+ if (!mEnabled) {
+ return false;
+ }
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "disabling");
+ }
+ mEnabled = false;
+ mBrightnessSensor.setEnabled(false);
+ mBrightnessFilter.clear();
+ mColorTemperatureSensor.setEnabled(false);
+ mColorTemperatureFilter.clear();
+ mThrottler.clear();
+ mAmbientColorTemperature = -1.0f;
+ mPendingAmbientColorTemperature = -1.0f;
+ return true;
+ }
+
+}
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java
new file mode 100644
index 000000000000..fd78ddbda9c8
--- /dev/null
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2019 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.whitebalance;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.util.TypedValue;
+
+/**
+ * The DisplayWhiteBalanceFactory creates and configures an DisplayWhiteBalanceController.
+ */
+public class DisplayWhiteBalanceFactory {
+
+ private static final String BRIGHTNESS_FILTER_TAG = "AmbientBrightnessFilter";
+ private static final String COLOR_TEMPERATURE_FILTER_TAG = "AmbientColorTemperatureFilter";
+
+ /**
+ * Create and configure an DisplayWhiteBalanceController.
+ *
+ * @param handler
+ * The handler used to determine which thread to run on.
+ * @param sensorManager
+ * The sensor manager used to acquire necessary sensors.
+ * @param resources
+ * The resources used to configure the various components.
+ *
+ * @return An DisplayWhiteBalanceController.
+ *
+ * @throws NullPointerException
+ * - handler is null;
+ * - sensorManager is null.
+ * @throws Resources.NotFoundException
+ * - Configurations are missing.
+ * @throws IllegalArgumentException
+ * - Configurations are invalid.
+ * @throws IllegalStateException
+ * - Cannot find the necessary sensors.
+ */
+ public static DisplayWhiteBalanceController create(Handler handler,
+ SensorManager sensorManager, Resources resources) {
+ final AmbientSensor.AmbientBrightnessSensor brightnessSensor =
+ createBrightnessSensor(handler, sensorManager, resources);
+ final AmbientFilter brightnessFilter = createBrightnessFilter(resources);
+ final AmbientSensor.AmbientColorTemperatureSensor colorTemperatureSensor =
+ createColorTemperatureSensor(handler, sensorManager, resources);
+ final AmbientFilter colorTemperatureFilter = createColorTemperatureFilter(resources);
+ final DisplayWhiteBalanceThrottler throttler = createThrottler(resources);
+ final float lowLightAmbientBrightnessThreshold = getFloat(resources,
+ com.android.internal.R.dimen
+ .config_displayWhiteBalanceLowLightAmbientBrightnessThreshold);
+ final float lowLightAmbientColorTemperature = getFloat(resources,
+ com.android.internal.R.dimen
+ .config_displayWhiteBalanceLowLightAmbientColorTemperature);
+ final DisplayWhiteBalanceController controller = new DisplayWhiteBalanceController(
+ brightnessSensor, brightnessFilter, colorTemperatureSensor, colorTemperatureFilter,
+ throttler, lowLightAmbientBrightnessThreshold, lowLightAmbientColorTemperature);
+ brightnessSensor.setCallbacks(controller);
+ colorTemperatureSensor.setCallbacks(controller);
+ return controller;
+ }
+
+ // Instantiation is disabled.
+ private DisplayWhiteBalanceFactory() { }
+
+ private static AmbientSensor.AmbientBrightnessSensor createBrightnessSensor(Handler handler,
+ SensorManager sensorManager, Resources resources) {
+ final int rate = resources.getInteger(
+ com.android.internal.R.integer.config_displayWhiteBalanceBrightnessSensorRate);
+ return new AmbientSensor.AmbientBrightnessSensor(handler, sensorManager, rate);
+ }
+
+ private static AmbientFilter createBrightnessFilter(Resources resources) {
+ final int horizon = resources.getInteger(
+ com.android.internal.R.integer.config_displayWhiteBalanceBrightnessFilterHorizon);
+ final float intercept = getFloat(resources,
+ com.android.internal.R.dimen.config_displayWhiteBalanceBrightnessFilterIntercept);
+ if (!Float.isNaN(intercept)) {
+ return new AmbientFilter.WeightedMovingAverageAmbientFilter(
+ BRIGHTNESS_FILTER_TAG, horizon, intercept);
+ }
+ throw new IllegalArgumentException("missing configurations: "
+ + "expected config_displayWhiteBalanceBrightnessFilterIntercept");
+ }
+
+
+ private static AmbientSensor.AmbientColorTemperatureSensor createColorTemperatureSensor(
+ Handler handler, SensorManager sensorManager, Resources resources) {
+ final String name = resources.getString(
+ com.android.internal.R.string
+ .config_displayWhiteBalanceColorTemperatureSensorName);
+ final int rate = resources.getInteger(
+ com.android.internal.R.integer
+ .config_displayWhiteBalanceColorTemperatureSensorRate);
+ return new AmbientSensor.AmbientColorTemperatureSensor(handler, sensorManager, name, rate);
+ }
+
+ private static AmbientFilter createColorTemperatureFilter(Resources resources) {
+ final int horizon = resources.getInteger(
+ com.android.internal.R.integer
+ .config_displayWhiteBalanceColorTemperatureFilterHorizon);
+ final float intercept = getFloat(resources,
+ com.android.internal.R.dimen
+ .config_displayWhiteBalanceColorTemperatureFilterIntercept);
+ if (!Float.isNaN(intercept)) {
+ return new AmbientFilter.WeightedMovingAverageAmbientFilter(
+ COLOR_TEMPERATURE_FILTER_TAG, horizon, intercept);
+ }
+ throw new IllegalArgumentException("missing configurations: "
+ + "expected config_displayWhiteBalanceColorTemperatureFilterIntercept");
+ }
+
+ private static DisplayWhiteBalanceThrottler createThrottler(Resources resources) {
+ final int increaseDebounce = resources.getInteger(
+ com.android.internal.R.integer.config_displayWhiteBalanceDecreaseDebounce);
+ final int decreaseDebounce = resources.getInteger(
+ com.android.internal.R.integer.config_displayWhiteBalanceIncreaseDebounce);
+ final float[] baseThresholds = getFloatArray(resources,
+ com.android.internal.R.array.config_displayWhiteBalanceBaseThresholds);
+ final float[] increaseThresholds = getFloatArray(resources,
+ com.android.internal.R.array.config_displayWhiteBalanceIncreaseThresholds);
+ final float[] decreaseThresholds = getFloatArray(resources,
+ com.android.internal.R.array.config_displayWhiteBalanceDecreaseThresholds);
+ return new DisplayWhiteBalanceThrottler(increaseDebounce, decreaseDebounce, baseThresholds,
+ increaseThresholds, decreaseThresholds);
+ }
+
+ private static float getFloat(Resources resources, int id) {
+ TypedValue value = new TypedValue();
+ resources.getValue(id, value, true /* resolveRefs */);
+ if (value.type != TypedValue.TYPE_FLOAT) {
+ return Float.NaN;
+ }
+ return value.getFloat();
+ }
+
+ private static float[] getFloatArray(Resources resources, int id) {
+ TypedArray array = resources.obtainTypedArray(id);
+ try {
+ if (array.length() == 0) {
+ return null;
+ }
+ float[] values = new float[array.length()];
+ for (int i = 0; i < values.length; i++) {
+ values[i] = array.getFloat(i, Float.NaN);
+ if (Float.isNaN(values[i])) {
+ return null;
+ }
+ }
+ return values;
+ } finally {
+ array.recycle();
+ }
+ }
+
+}
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceSettings.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceSettings.java
new file mode 100644
index 000000000000..a53e91c622fb
--- /dev/null
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceSettings.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2019 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.whitebalance;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings.Secure;
+import android.util.Slog;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.LocalServices;
+import com.android.server.display.ColorDisplayService;
+import com.android.server.display.ColorDisplayService.ColorDisplayServiceInternal;
+import com.android.server.display.whitebalance.DisplayWhiteBalanceController.Callbacks;
+
+import java.io.PrintWriter;
+
+/**
+ * The DisplayWhiteBalanceSettings holds the state of all the settings related to
+ * display white-balance, and can be used to decide whether to enable the
+ * DisplayWhiteBalanceController.
+ */
+public class DisplayWhiteBalanceSettings implements
+ ColorDisplayService.DisplayWhiteBalanceListener {
+
+ protected static final String TAG = "DisplayWhiteBalanceSettings";
+ protected boolean mLoggingEnabled;
+
+ private static final String SETTING_URI = Secure.DISPLAY_WHITE_BALANCE_ENABLED;
+ private static final int SETTING_DEFAULT = 0;
+ private static final int SETTING_ENABLED = 1;
+
+ private static final int MSG_SET_ACTIVE = 1;
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final SettingsObserver mSettingsObserver;
+
+ // To decouple the DisplayPowerController from the DisplayWhiteBalanceSettings, the DPC
+ // implements Callbacks and passes itself to the DWBS so it can call back into it without
+ // knowing about it.
+ private Callbacks mCallbacks;
+
+ private int mSetting;
+ private boolean mActive;
+
+ /**
+ * @param context
+ * The context in which display white-balance is used.
+ * @param handler
+ * The handler used to determine which thread to run on.
+ *
+ * @throws NullPointerException
+ * - context is null;
+ * - handler is null.
+ */
+ public DisplayWhiteBalanceSettings(@NonNull Context context, @NonNull Handler handler) {
+ validateArguments(context, handler);
+ mLoggingEnabled = false;
+ mContext = context;
+ mHandler = new DisplayWhiteBalanceSettingsHandler(handler.getLooper());
+ mSettingsObserver = new SettingsObserver(mHandler);
+ mSetting = getSetting();
+ mActive = false;
+ mCallbacks = null;
+
+ mContext.getContentResolver().registerContentObserver(
+ Secure.getUriFor(SETTING_URI), false /* notifyForDescendants */, mSettingsObserver,
+ UserHandle.USER_ALL);
+
+ ColorDisplayServiceInternal cds =
+ LocalServices.getService(ColorDisplayServiceInternal.class);
+ cds.setDisplayWhiteBalanceListener(this);
+ }
+
+ /**
+ * Set an object to call back to when the display white balance state should be updated.
+ *
+ * @param callbacks
+ * The object to call back to.
+ *
+ * @return Whether the method suceeded or not.
+ */
+ public boolean setCallbacks(Callbacks callbacks) {
+ if (mCallbacks == callbacks) {
+ return false;
+ }
+ mCallbacks = callbacks;
+ return true;
+ }
+
+ /**
+ * Enable/disable logging.
+ *
+ * @param loggingEnabled
+ * Whether logging should be on/off.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setLoggingEnabled(boolean loggingEnabled) {
+ if (mLoggingEnabled == loggingEnabled) {
+ return false;
+ }
+ mLoggingEnabled = loggingEnabled;
+ return true;
+ }
+
+ /**
+ * Returns whether display white-balance is enabled.
+ *
+ * @return Whether display white-balance is enabled.
+ */
+ public boolean isEnabled() {
+ return (mSetting == SETTING_ENABLED) && mActive;
+ }
+
+ /**
+ * Re-evaluate state after switching to a new user.
+ */
+ public void onSwitchUser() {
+ handleSettingChange();
+ }
+
+ /**
+ * Dump the state.
+ *
+ * @param writer
+ * The writer used to dump the state.
+ */
+ public void dump(PrintWriter writer) {
+ writer.println("DisplayWhiteBalanceSettings");
+ writer.println(" mLoggingEnabled=" + mLoggingEnabled);
+ writer.println(" mContext=" + mContext);
+ writer.println(" mHandler=" + mHandler);
+ writer.println(" mSettingsObserver=" + mSettingsObserver);
+ writer.println(" mSetting=" + mSetting);
+ writer.println(" mActive=" + mActive);
+ writer.println(" mCallbacks=" + mCallbacks);
+ }
+
+ @Override
+ public void onDisplayWhiteBalanceStatusChanged(boolean active) {
+ Message msg = mHandler.obtainMessage(MSG_SET_ACTIVE, active ? 1 : 0, 0);
+ msg.sendToTarget();
+ }
+
+ private void validateArguments(Context context, Handler handler) {
+ Preconditions.checkNotNull(context, "context must not be null");
+ Preconditions.checkNotNull(handler, "handler must not be null");
+ }
+
+ private int getSetting() {
+ return Secure.getIntForUser(mContext.getContentResolver(), SETTING_URI, SETTING_DEFAULT,
+ UserHandle.USER_CURRENT);
+ }
+
+ private void handleSettingChange() {
+ final int setting = getSetting();
+ if (mSetting == setting) {
+ return;
+ }
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "Setting: " + setting);
+ }
+ mSetting = setting;
+ if (mCallbacks != null) {
+ mCallbacks.updateWhiteBalance();
+ }
+ }
+
+ private void setActive(boolean active) {
+ if (mActive == active) {
+ return;
+ }
+ if (mLoggingEnabled) {
+ Slog.d(TAG, "Active: " + active);
+ }
+ mActive = active;
+ if (mCallbacks != null) {
+ mCallbacks.updateWhiteBalance();
+ }
+ }
+
+ private final class SettingsObserver extends ContentObserver {
+ SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ handleSettingChange();
+ }
+ }
+
+ private final class DisplayWhiteBalanceSettingsHandler extends Handler {
+ DisplayWhiteBalanceSettingsHandler(Looper looper) {
+ super(looper, null, true /* async */);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SET_ACTIVE:
+ setActive(msg.arg1 != 0);
+ break;
+ }
+ }
+ }
+
+}
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceThrottler.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceThrottler.java
new file mode 100644
index 000000000000..c1f0e983a366
--- /dev/null
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceThrottler.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2019 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.whitebalance;
+
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+/**
+ * The DisplayWhiteBalanceController uses the DisplayWhiteBalanceThrottler to decide whether the
+ * screen color temperature should be updated, suppressing changes that are too frequent or too
+ * minor.
+ */
+class DisplayWhiteBalanceThrottler {
+
+ protected static final String TAG = "DisplayWhiteBalanceThrottler";
+ protected boolean mLoggingEnabled;
+
+ private int mIncreaseDebounce; // Milliseconds
+ private int mDecreaseDebounce; // Milliseconds
+ private long mLastTime; // Milliseconds
+
+ private float[] mBaseThresholds;
+ private float[] mIncreaseThresholds;
+ private float[] mDecreaseThresholds;
+ private float mIncreaseThreshold;
+ private float mDecreaseThreshold;
+ private float mLastValue;
+
+ /**
+ * @param increaseDebounce
+ * The debounce time for increasing (throttled if {@code time < lastTime + debounce}).
+ * @param decreaseDebounce
+ * The debounce time for decreasing (throttled if {@code time < lastTime + debounce}).
+ * @param baseThresholds
+ * The ambient color temperature values used to determine the threshold as the
+ * corresponding value in increaseThresholds/decreaseThresholds.
+ * @param increaseThresholds
+ * The increase threshold values (throttled if {@code value < value * (1 + threshold)}).
+ * @param decreaseThresholds
+ * The decrease threshold values (throttled if {@code value > value * (1 - threshold)}).
+ *
+ * @throws IllegalArgumentException
+ * - increaseDebounce is negative;
+ * - decreaseDebounce is negative;
+ * - baseThresholds to increaseThresholds is not a valid mapping*;
+ * - baseThresholds to decreaseThresholds is not a valid mapping*;
+ *
+ * (*) The x to y mapping is valid if:
+ * - x and y are not null;
+ * - x and y are not empty;
+ * - x and y contain only non-negative numbers;
+ * - x is strictly increasing.
+ */
+ DisplayWhiteBalanceThrottler(int increaseDebounce, int decreaseDebounce,
+ float[] baseThresholds, float[] increaseThresholds, float[] decreaseThresholds) {
+ validateArguments(increaseDebounce, decreaseDebounce, baseThresholds, increaseThresholds,
+ decreaseThresholds);
+ mLoggingEnabled = false;
+ mIncreaseDebounce = increaseDebounce;
+ mDecreaseDebounce = decreaseDebounce;
+ mBaseThresholds = baseThresholds;
+ mIncreaseThresholds = increaseThresholds;
+ mDecreaseThresholds = decreaseThresholds;
+ clear();
+ }
+
+ /**
+ * Check whether the ambient color temperature should be throttled.
+ *
+ * @param value
+ * The ambient color temperature value.
+ *
+ * @return Whether the ambient color temperature should be throttled.
+ */
+ public boolean throttle(float value) {
+ if (mLastTime != -1 && (tooSoon(value) || tooClose(value))) {
+ return true;
+ }
+ computeThresholds(value);
+ mLastTime = System.currentTimeMillis();
+ mLastValue = value;
+ return false;
+ }
+
+ /**
+ * Clears the throttler state.
+ */
+ public void clear() {
+ mLastTime = -1;
+ mIncreaseThreshold = -1.0f;
+ mDecreaseThreshold = -1.0f;
+ mLastValue = -1.0f;
+ }
+
+ /**
+ * Enable/disable logging.
+ *
+ * @param loggingEnabled
+ * Whether logging is on/off.
+ *
+ * @return Whether the method succeeded or not.
+ */
+ public boolean setLoggingEnabled(boolean loggingEnabled) {
+ if (mLoggingEnabled == loggingEnabled) {
+ return false;
+ }
+ mLoggingEnabled = loggingEnabled;
+ return true;
+ }
+
+ /**
+ * Dump the state.
+ *
+ * @param writer
+ * The PrintWriter used to dump the state.
+ */
+ public void dump(PrintWriter writer) {
+ writer.println(" DisplayWhiteBalanceThrottler");
+ writer.println(" mLoggingEnabled=" + mLoggingEnabled);
+ writer.println(" mIncreaseDebounce=" + mIncreaseDebounce);
+ writer.println(" mDecreaseDebounce=" + mDecreaseDebounce);
+ writer.println(" mLastTime=" + mLastTime);
+ writer.println(" mBaseThresholds=" + Arrays.toString(mBaseThresholds));
+ writer.println(" mIncreaseThresholds=" + Arrays.toString(mIncreaseThresholds));
+ writer.println(" mDecreaseThresholds=" + Arrays.toString(mDecreaseThresholds));
+ writer.println(" mIncreaseThreshold=" + mIncreaseThreshold);
+ writer.println(" mDecreaseThreshold=" + mDecreaseThreshold);
+ writer.println(" mLastValue=" + mLastValue);
+ }
+
+ private void validateArguments(float increaseDebounce, float decreaseDebounce,
+ float[] baseThresholds, float[] increaseThresholds, float[] decreaseThresholds) {
+ if (Float.isNaN(increaseDebounce) || increaseDebounce < 0.0f) {
+ throw new IllegalArgumentException("increaseDebounce must be a non-negative number.");
+ }
+ if (Float.isNaN(decreaseDebounce) || decreaseDebounce < 0.0f) {
+ throw new IllegalArgumentException("decreaseDebounce must be a non-negative number.");
+ }
+ if (!isValidMapping(baseThresholds, increaseThresholds)) {
+ throw new IllegalArgumentException(
+ "baseThresholds to increaseThresholds is not a valid mapping.");
+ }
+ if (!isValidMapping(baseThresholds, decreaseThresholds)) {
+ throw new IllegalArgumentException(
+ "baseThresholds to decreaseThresholds is not a valid mapping.");
+ }
+ }
+
+ private static boolean isValidMapping(float[] x, float[] y) {
+ if (x == null || y == null || x.length == 0 || y.length == 0 || x.length != y.length) {
+ return false;
+ }
+ float prevX = -1.0f;
+ for (int i = 0; i < x.length; i++) {
+ if (Float.isNaN(x[i]) || Float.isNaN(y[i]) || x[i] < 0 || prevX >= x[i]) {
+ return false;
+ }
+ prevX = x[i];
+ }
+ return true;
+ }
+
+ private boolean tooSoon(float value) {
+ final long time = System.currentTimeMillis();
+ final long earliestTime;
+ if (value > mLastValue) {
+ earliestTime = mLastTime + mIncreaseDebounce;
+ } else { // value <= mLastValue
+ earliestTime = mLastTime + mDecreaseDebounce;
+ }
+ final boolean tooSoon = time < earliestTime;
+ if (mLoggingEnabled) {
+ Slog.d(TAG, (tooSoon ? "too soon: " : "late enough: ") + time
+ + (tooSoon ? " < " : " > ") + earliestTime);
+ }
+ return tooSoon;
+ }
+
+ private boolean tooClose(float value) {
+ final float threshold;
+ final boolean tooClose;
+ if (value > mLastValue) {
+ threshold = mIncreaseThreshold;
+ tooClose = value < threshold;
+ } else { // value <= mLastValue
+ threshold = mDecreaseThreshold;
+ tooClose = value > threshold;
+ }
+ if (mLoggingEnabled) {
+ Slog.d(TAG, (tooClose ? "too close: " : "far enough: ") + value
+ + (value > threshold ? " > " : " < ") + threshold);
+ }
+ return tooClose;
+ }
+
+ private void computeThresholds(float value) {
+ final int index = getHighestIndexBefore(value, mBaseThresholds);
+ mIncreaseThreshold = value * (1.0f + mIncreaseThresholds[index]);
+ mDecreaseThreshold = value * (1.0f - mDecreaseThresholds[index]);
+ }
+
+ private int getHighestIndexBefore(float value, float[] values) {
+ for (int i = 0; i < values.length; i++) {
+ if (values[i] >= value) {
+ return i;
+ }
+ }
+ return values.length - 1;
+ }
+
+}