diff options
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; + } + +} |