/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.display; import com.android.server.EventLogTags; import com.android.server.LocalServices; import android.annotation.Nullable; import android.app.ActivityManager; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.hardware.display.BrightnessConfiguration; import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.SystemClock; import android.os.Trace; import android.text.format.DateUtils; import android.util.EventLog; import android.util.MathUtils; import android.util.Slog; import android.util.TimeUtils; import java.io.PrintWriter; class AutomaticBrightnessController { private static final String TAG = "AutomaticBrightnessController"; private static final boolean DEBUG = false; private static final boolean DEBUG_PRETEND_LIGHT_SENSOR_ABSENT = false; // If true, enables the use of the screen auto-brightness adjustment setting. private static final boolean USE_SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT = true; // How long the current sensor reading is assumed to be valid beyond the current time. // This provides a bit of prediction, as well as ensures that the weight for the last sample is // non-zero, which in turn ensures that the total weight is non-zero. private static final long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100; // Debounce for sampling user-initiated changes in display brightness to ensure // the user is satisfied with the result before storing the sample. private static final int BRIGHTNESS_ADJUSTMENT_SAMPLE_DEBOUNCE_MILLIS = 10000; // Timeout after which we remove the effects any user interactions might've had on the // brightness mapping. This timeout doesn't start until we transition to a non-interactive // display policy so that we don't reset while users are using their devices, but also so that // we don't erroneously keep the short-term model if the device is dozing but the display is // fully on. private static final int SHORT_TERM_MODEL_TIMEOUT_MILLIS = 30000; private static final int MSG_UPDATE_AMBIENT_LUX = 1; private static final int MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE = 2; private static final int MSG_INVALIDATE_SHORT_TERM_MODEL = 3; // Length of the ambient light horizon used to calculate the long term estimate of ambient // light. private static final int AMBIENT_LIGHT_LONG_HORIZON_MILLIS = 10000; // Length of the ambient light horizon used to calculate short-term estimate of ambient light. private static final int AMBIENT_LIGHT_SHORT_HORIZON_MILLIS = 2000; // Callbacks for requesting updates to the display's power state private final Callbacks mCallbacks; // The sensor manager. private final SensorManager mSensorManager; // The light sensor, or null if not available or needed. private final Sensor mLightSensor; // The mapper to translate ambient lux to screen brightness in the range [0, 1.0]. private final BrightnessMappingStrategy mBrightnessMapper; // The minimum and maximum screen brightnesses. private final int mScreenBrightnessRangeMinimum; private final int mScreenBrightnessRangeMaximum; // How much to scale doze brightness by (should be (0, 1.0]). private final float mDozeScaleFactor; // Initial light sensor event rate in milliseconds. private final int mInitialLightSensorRate; // Steady-state light sensor event rate in milliseconds. private final int mNormalLightSensorRate; // The current light sensor event rate in milliseconds. private int mCurrentLightSensorRate; // Stability requirements in milliseconds for accepting a new brightness level. This is used // for debouncing the light sensor. Different constants are used to debounce the light sensor // when adapting to brighter or darker environments. This parameter controls how quickly // brightness changes occur in response to an observed change in light level that exceeds the // hysteresis threshold. private final long mBrighteningLightDebounceConfig; private final long mDarkeningLightDebounceConfig; // If true immediately after the screen is turned on the controller will try to adjust the // brightness based on the current sensor reads. If false, the controller will collect more data // and only then decide whether to change brightness. private final boolean mResetAmbientLuxAfterWarmUpConfig; // Period of time in which to consider light samples in milliseconds. private final int mAmbientLightHorizon; // The intercept used for the weighting calculation. This is used in order to keep all possible // weighting values positive. private final int mWeightingIntercept; // Configuration object for determining thresholds to change brightness dynamically private final HysteresisLevels mHysteresisLevels; // Amount of time to delay auto-brightness after screen on while waiting for // the light sensor to warm-up in milliseconds. // May be 0 if no warm-up is required. private int mLightSensorWarmUpTimeConfig; // Set to true if the light sensor is enabled. private boolean mLightSensorEnabled; // The time when the light sensor was enabled. private long mLightSensorEnableTime; // The currently accepted nominal ambient light level. private float mAmbientLux; // True if mAmbientLux holds a valid value. private boolean mAmbientLuxValid; // The ambient light level threshold at which to brighten or darken the screen. private float mBrighteningLuxThreshold; private float mDarkeningLuxThreshold; // The most recent light sample. private float mLastObservedLux; // The time of the most light recent sample. private long mLastObservedLuxTime; // The number of light samples collected since the light sensor was enabled. private int mRecentLightSamples; // A ring buffer containing all of the recent ambient light sensor readings. private AmbientLightRingBuffer mAmbientLightRingBuffer; // The handler private AutomaticBrightnessHandler mHandler; // The screen brightness level that has been chosen by the auto-brightness // algorithm. The actual brightness should ramp towards this value. // We preserve this value even when we stop using the light sensor so // that we can quickly revert to the previous auto-brightness level // while the light sensor warms up. // Use -1 if there is no current auto-brightness value available. private int mScreenAutoBrightness = -1; // The current display policy. This is useful, for example, for knowing when we're dozing, // where the light sensor may not be available. private int mDisplayPolicy = DisplayPowerRequest.POLICY_OFF; // True if we are collecting a brightness adjustment sample, along with some data // for the initial state of the sample. private boolean mBrightnessAdjustmentSamplePending; private float mBrightnessAdjustmentSampleOldLux; private int mBrightnessAdjustmentSampleOldBrightness; // When the short term model is invalidated, we don't necessarily reset it (i.e. clear the // user's adjustment) immediately, but wait for a drastic enough change in the ambient light. // The anchor determines what were the light levels when the user has set her preference, and // we use a relative threshold to determine when to revert to the OEM curve. private boolean mShortTermModelValid; private float mShortTermModelAnchor; private float SHORT_TERM_MODEL_THRESHOLD_RATIO = 0.6f; public AutomaticBrightnessController(Callbacks callbacks, Looper looper, SensorManager sensorManager, BrightnessMappingStrategy mapper, int lightSensorWarmUpTime, int brightnessMin, int brightnessMax, float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate, long brighteningLightDebounceConfig, long darkeningLightDebounceConfig, boolean resetAmbientLuxAfterWarmUpConfig, HysteresisLevels hysteresisLevels) { mCallbacks = callbacks; mSensorManager = sensorManager; mBrightnessMapper = mapper; mScreenBrightnessRangeMinimum = brightnessMin; mScreenBrightnessRangeMaximum = brightnessMax; mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime; mDozeScaleFactor = dozeScaleFactor; mNormalLightSensorRate = lightSensorRate; mInitialLightSensorRate = initialLightSensorRate; mCurrentLightSensorRate = -1; mBrighteningLightDebounceConfig = brighteningLightDebounceConfig; mDarkeningLightDebounceConfig = darkeningLightDebounceConfig; mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig; mAmbientLightHorizon = AMBIENT_LIGHT_LONG_HORIZON_MILLIS; mWeightingIntercept = AMBIENT_LIGHT_LONG_HORIZON_MILLIS; mHysteresisLevels = hysteresisLevels; mShortTermModelValid = true; mShortTermModelAnchor = -1; mHandler = new AutomaticBrightnessHandler(looper); mAmbientLightRingBuffer = new AmbientLightRingBuffer(mNormalLightSensorRate, mAmbientLightHorizon); if (!DEBUG_PRETEND_LIGHT_SENSOR_ABSENT) { mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); } } public int getAutomaticScreenBrightness() { if (!mAmbientLuxValid) { return -1; } if (mDisplayPolicy == DisplayPowerRequest.POLICY_DOZE) { return (int) (mScreenAutoBrightness * mDozeScaleFactor); } return mScreenAutoBrightness; } public float getAutomaticScreenBrightnessAdjustment() { return mBrightnessMapper.getAutoBrightnessAdjustment(); } public void configure(boolean enable, @Nullable BrightnessConfiguration configuration, float brightness, boolean userChangedBrightness, float adjustment, boolean userChangedAutoBrightnessAdjustment, int displayPolicy) { // While dozing, the application processor may be suspended which will prevent us from // receiving new information from the light sensor. On some devices, we may be able to // switch to a wake-up light sensor instead but for now we will simply disable the sensor // and hold onto the last computed screen auto brightness. We save the dozing flag for // debugging purposes. boolean dozing = (displayPolicy == DisplayPowerRequest.POLICY_DOZE); boolean changed = setBrightnessConfiguration(configuration); changed |= setDisplayPolicy(displayPolicy); if (userChangedAutoBrightnessAdjustment) { changed |= setAutoBrightnessAdjustment(adjustment); } if (userChangedBrightness && enable) { // Update the brightness curve with the new user control point. It's critical this // happens after we update the autobrightness adjustment since it may reset it. changed |= setScreenBrightnessByUser(brightness); } final boolean userInitiatedChange = userChangedBrightness || userChangedAutoBrightnessAdjustment; if (userInitiatedChange && enable && !dozing) { prepareBrightnessAdjustmentSample(); } changed |= setLightSensorEnabled(enable && !dozing); if (changed) { updateAutoBrightness(false /*sendUpdate*/); } } public boolean hasUserDataPoints() { return mBrightnessMapper.hasUserDataPoints(); } public boolean isDefaultConfig() { return mBrightnessMapper.isDefaultConfig(); } public BrightnessConfiguration getDefaultConfig() { return mBrightnessMapper.getDefaultConfig(); } private boolean setDisplayPolicy(int policy) { if (mDisplayPolicy == policy) { return false; } final int oldPolicy = mDisplayPolicy; mDisplayPolicy = policy; if (DEBUG) { Slog.d(TAG, "Display policy transitioning from " + oldPolicy + " to " + policy); } if (!isInteractivePolicy(policy) && isInteractivePolicy(oldPolicy)) { mHandler.sendEmptyMessageDelayed(MSG_INVALIDATE_SHORT_TERM_MODEL, SHORT_TERM_MODEL_TIMEOUT_MILLIS); } else if (isInteractivePolicy(policy) && !isInteractivePolicy(oldPolicy)) { mHandler.removeMessages(MSG_INVALIDATE_SHORT_TERM_MODEL); } return true; } private static boolean isInteractivePolicy(int policy) { return policy == DisplayPowerRequest.POLICY_BRIGHT || policy == DisplayPowerRequest.POLICY_DIM || policy == DisplayPowerRequest.POLICY_VR; } private boolean setScreenBrightnessByUser(float brightness) { if (!mAmbientLuxValid) { // If we don't have a valid ambient lux then we don't have a valid brightness anyways, // and we can't use this data to add a new control point to the short-term model. return false; } mBrightnessMapper.addUserDataPoint(mAmbientLux, brightness); mShortTermModelValid = true; mShortTermModelAnchor = mAmbientLux; if (DEBUG) { Slog.d(TAG, "ShortTermModel: anchor=" + mShortTermModelAnchor); } return true; } public void resetShortTermModel() { mBrightnessMapper.clearUserDataPoints(); mShortTermModelValid = true; mShortTermModelAnchor = -1; } private void invalidateShortTermModel() { if (DEBUG) { Slog.d(TAG, "ShortTermModel: invalidate user data"); } mShortTermModelValid = false; } public boolean setBrightnessConfiguration(BrightnessConfiguration configuration) { if (mBrightnessMapper.setBrightnessConfiguration(configuration)) { resetShortTermModel(); return true; } return false; } public void dump(PrintWriter pw) { pw.println(); pw.println("Automatic Brightness Controller Configuration:"); pw.println(" mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum); pw.println(" mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum); pw.println(" mDozeScaleFactor=" + mDozeScaleFactor); pw.println(" mInitialLightSensorRate=" + mInitialLightSensorRate); pw.println(" mNormalLightSensorRate=" + mNormalLightSensorRate); pw.println(" mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig); pw.println(" mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig); pw.println(" mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig); pw.println(" mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig); pw.println(" mAmbientLightHorizon=" + mAmbientLightHorizon); pw.println(" mWeightingIntercept=" + mWeightingIntercept); pw.println(); pw.println("Automatic Brightness Controller State:"); pw.println(" mLightSensor=" + mLightSensor); pw.println(" mLightSensorEnabled=" + mLightSensorEnabled); pw.println(" mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime)); pw.println(" mCurrentLightSensorRate=" + mCurrentLightSensorRate); pw.println(" mAmbientLux=" + mAmbientLux); pw.println(" mAmbientLuxValid=" + mAmbientLuxValid); pw.println(" mBrighteningLuxThreshold=" + mBrighteningLuxThreshold); pw.println(" mDarkeningLuxThreshold=" + mDarkeningLuxThreshold); pw.println(" mLastObservedLux=" + mLastObservedLux); pw.println(" mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime)); pw.println(" mRecentLightSamples=" + mRecentLightSamples); pw.println(" mAmbientLightRingBuffer=" + mAmbientLightRingBuffer); pw.println(" mScreenAutoBrightness=" + mScreenAutoBrightness); pw.println(" mDisplayPolicy=" + DisplayPowerRequest.policyToString(mDisplayPolicy)); pw.println(" mShortTermModelAnchor=" + mShortTermModelAnchor); pw.println(" mShortTermModelValid=" + mShortTermModelValid); pw.println(" mBrightnessAdjustmentSamplePending=" + mBrightnessAdjustmentSamplePending); pw.println(" mBrightnessAdjustmentSampleOldLux=" + mBrightnessAdjustmentSampleOldLux); pw.println(" mBrightnessAdjustmentSampleOldBrightness=" + mBrightnessAdjustmentSampleOldBrightness); pw.println(" mShortTermModelValid=" + mShortTermModelValid); pw.println(); mBrightnessMapper.dump(pw); pw.println(); mHysteresisLevels.dump(pw); } private boolean setLightSensorEnabled(boolean enable) { if (enable) { if (!mLightSensorEnabled) { mLightSensorEnabled = true; mLightSensorEnableTime = SystemClock.uptimeMillis(); mCurrentLightSensorRate = mInitialLightSensorRate; mSensorManager.registerListener(mLightSensorListener, mLightSensor, mCurrentLightSensorRate * 1000, mHandler); return true; } } else if (mLightSensorEnabled) { mLightSensorEnabled = false; mAmbientLuxValid = !mResetAmbientLuxAfterWarmUpConfig; mRecentLightSamples = 0; mAmbientLightRingBuffer.clear(); mCurrentLightSensorRate = -1; mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX); mSensorManager.unregisterListener(mLightSensorListener); } return false; } private void handleLightSensorEvent(long time, float lux) { Trace.traceCounter(Trace.TRACE_TAG_POWER, "ALS", (int) lux); mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX); if (mAmbientLightRingBuffer.size() == 0) { // switch to using the steady-state sample rate after grabbing the initial light sample adjustLightSensorRate(mNormalLightSensorRate); } applyLightSensorMeasurement(time, lux); updateAmbientLux(time); } private void applyLightSensorMeasurement(long time, float lux) { mRecentLightSamples++; mAmbientLightRingBuffer.prune(time - mAmbientLightHorizon); mAmbientLightRingBuffer.push(time, lux); // Remember this sample value. mLastObservedLux = lux; mLastObservedLuxTime = time; } private void adjustLightSensorRate(int lightSensorRate) { // if the light sensor rate changed, update the sensor listener if (lightSensorRate != mCurrentLightSensorRate) { if (DEBUG) { Slog.d(TAG, "adjustLightSensorRate: " + "previousRate=" + mCurrentLightSensorRate + ", " + "currentRate=" + lightSensorRate); } mCurrentLightSensorRate = lightSensorRate; mSensorManager.unregisterListener(mLightSensorListener); mSensorManager.registerListener(mLightSensorListener, mLightSensor, lightSensorRate * 1000, mHandler); } } private boolean setAutoBrightnessAdjustment(float adjustment) { return mBrightnessMapper.setAutoBrightnessAdjustment(adjustment); } private void setAmbientLux(float lux) { if (DEBUG) { Slog.d(TAG, "setAmbientLux(" + lux + ")"); } if (lux < 0) { Slog.w(TAG, "Ambient lux was negative, ignoring and setting to 0"); lux = 0; } mAmbientLux = lux; mBrighteningLuxThreshold = mHysteresisLevels.getBrighteningThreshold(lux); mDarkeningLuxThreshold = mHysteresisLevels.getDarkeningThreshold(lux); // If the short term model was invalidated and the change is drastic enough, reset it. if (!mShortTermModelValid && mShortTermModelAnchor != -1) { final float minAmbientLux = mShortTermModelAnchor - mShortTermModelAnchor * SHORT_TERM_MODEL_THRESHOLD_RATIO; final float maxAmbientLux = mShortTermModelAnchor + mShortTermModelAnchor * SHORT_TERM_MODEL_THRESHOLD_RATIO; if (minAmbientLux < mAmbientLux && mAmbientLux < maxAmbientLux) { if (DEBUG) { Slog.d(TAG, "ShortTermModel: re-validate user data, ambient lux is " + minAmbientLux + " < " + mAmbientLux + " < " + maxAmbientLux); } mShortTermModelValid = true; } else { Slog.d(TAG, "ShortTermModel: reset data, ambient lux is " + mAmbientLux + "(" + minAmbientLux + ", " + maxAmbientLux + ")"); resetShortTermModel(); } } } private float calculateAmbientLux(long now, long horizon) { if (DEBUG) { Slog.d(TAG, "calculateAmbientLux(" + now + ", " + horizon + ")"); } final int N = mAmbientLightRingBuffer.size(); if (N == 0) { Slog.e(TAG, "calculateAmbientLux: No ambient light readings available"); return -1; } // Find the first measurement that is just outside of the horizon. int endIndex = 0; final long horizonStartTime = now - horizon; for (int i = 0; i < N-1; i++) { if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) { endIndex++; } else { break; } } if (DEBUG) { Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" + mAmbientLightRingBuffer.getTime(endIndex) + ", " + mAmbientLightRingBuffer.getLux(endIndex) + ")"); } float sum = 0; float totalWeight = 0; long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS; for (int i = N - 1; i >= endIndex; i--) { long eventTime = mAmbientLightRingBuffer.getTime(i); if (i == endIndex && eventTime < horizonStartTime) { // If we're at the final value, make sure we only consider the part of the sample // within our desired horizon. eventTime = horizonStartTime; } final long startTime = eventTime - now; float weight = calculateWeight(startTime, endTime); float lux = mAmbientLightRingBuffer.getLux(i); if (DEBUG) { Slog.d(TAG, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " + "lux=" + lux + ", " + "weight=" + weight); } totalWeight += weight; sum += lux * weight; endTime = startTime; } if (DEBUG) { Slog.d(TAG, "calculateAmbientLux: " + "totalWeight=" + totalWeight + ", " + "newAmbientLux=" + (sum / totalWeight)); } return sum / totalWeight; } private float calculateWeight(long startDelta, long endDelta) { return weightIntegral(endDelta) - weightIntegral(startDelta); } // Evaluates the integral of y = x + mWeightingIntercept. This is always positive for the // horizon we're looking at and provides a non-linear weighting for light samples. private float weightIntegral(long x) { return x * (x * 0.5f + mWeightingIntercept); } private long nextAmbientLightBrighteningTransition(long time) { final int N = mAmbientLightRingBuffer.size(); long earliestValidTime = time; for (int i = N - 1; i >= 0; i--) { if (mAmbientLightRingBuffer.getLux(i) <= mBrighteningLuxThreshold) { break; } earliestValidTime = mAmbientLightRingBuffer.getTime(i); } return earliestValidTime + mBrighteningLightDebounceConfig; } private long nextAmbientLightDarkeningTransition(long time) { final int N = mAmbientLightRingBuffer.size(); long earliestValidTime = time; for (int i = N - 1; i >= 0; i--) { if (mAmbientLightRingBuffer.getLux(i) >= mDarkeningLuxThreshold) { break; } earliestValidTime = mAmbientLightRingBuffer.getTime(i); } return earliestValidTime + mDarkeningLightDebounceConfig; } private void updateAmbientLux() { long time = SystemClock.uptimeMillis(); mAmbientLightRingBuffer.prune(time - mAmbientLightHorizon); updateAmbientLux(time); } private void updateAmbientLux(long time) { // If the light sensor was just turned on then immediately update our initial // estimate of the current ambient light level. if (!mAmbientLuxValid) { final long timeWhenSensorWarmedUp = mLightSensorWarmUpTimeConfig + mLightSensorEnableTime; if (time < timeWhenSensorWarmedUp) { if (DEBUG) { Slog.d(TAG, "updateAmbientLux: Sensor not ready yet: " + "time=" + time + ", " + "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp); } mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, timeWhenSensorWarmedUp); return; } setAmbientLux(calculateAmbientLux(time, AMBIENT_LIGHT_SHORT_HORIZON_MILLIS)); mAmbientLuxValid = true; if (DEBUG) { Slog.d(TAG, "updateAmbientLux: Initializing: " + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " + "mAmbientLux=" + mAmbientLux); } updateAutoBrightness(true); } long nextBrightenTransition = nextAmbientLightBrighteningTransition(time); long nextDarkenTransition = nextAmbientLightDarkeningTransition(time); // Essentially, we calculate both a slow ambient lux, to ensure there's a true long-term // change in lighting conditions, and a fast ambient lux to determine what the new // brightness situation is since the slow lux can be quite slow to converge. // // Note that both values need to be checked for sufficient change before updating the // proposed ambient light value since the slow value might be sufficiently far enough away // from the fast value to cause a recalculation while its actually just converging on // the fast value still. float slowAmbientLux = calculateAmbientLux(time, AMBIENT_LIGHT_LONG_HORIZON_MILLIS); float fastAmbientLux = calculateAmbientLux(time, AMBIENT_LIGHT_SHORT_HORIZON_MILLIS); if ((slowAmbientLux >= mBrighteningLuxThreshold && fastAmbientLux >= mBrighteningLuxThreshold && nextBrightenTransition <= time) || (slowAmbientLux <= mDarkeningLuxThreshold && fastAmbientLux <= mDarkeningLuxThreshold && nextDarkenTransition <= time)) { setAmbientLux(fastAmbientLux); if (DEBUG) { Slog.d(TAG, "updateAmbientLux: " + ((fastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " + "mBrighteningLuxThreshold=" + mBrighteningLuxThreshold + ", " + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " + "mAmbientLux=" + mAmbientLux); } updateAutoBrightness(true); nextBrightenTransition = nextAmbientLightBrighteningTransition(time); nextDarkenTransition = nextAmbientLightDarkeningTransition(time); } long nextTransitionTime = Math.min(nextDarkenTransition, nextBrightenTransition); // If one of the transitions is ready to occur, but the total weighted ambient lux doesn't // exceed the necessary threshold, then it's possible we'll get a transition time prior to // now. Rather than continually checking to see whether the weighted lux exceeds the // threshold, schedule an update for when we'd normally expect another light sample, which // should be enough time to decide whether we should actually transition to the new // weighted ambient lux or not. nextTransitionTime = nextTransitionTime > time ? nextTransitionTime : time + mNormalLightSensorRate; if (DEBUG) { Slog.d(TAG, "updateAmbientLux: Scheduling ambient lux update for " + nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime)); } mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, nextTransitionTime); } private void updateAutoBrightness(boolean sendUpdate) { if (!mAmbientLuxValid) { return; } float value = mBrightnessMapper.getBrightness(mAmbientLux); int newScreenAutoBrightness = clampScreenBrightness(Math.round(value * PowerManager.BRIGHTNESS_ON)); if (mScreenAutoBrightness != newScreenAutoBrightness) { if (DEBUG) { Slog.d(TAG, "updateAutoBrightness: " + "mScreenAutoBrightness=" + mScreenAutoBrightness + ", " + "newScreenAutoBrightness=" + newScreenAutoBrightness); } mScreenAutoBrightness = newScreenAutoBrightness; if (sendUpdate) { mCallbacks.updateBrightness(); } } } private int clampScreenBrightness(int value) { return MathUtils.constrain(value, mScreenBrightnessRangeMinimum, mScreenBrightnessRangeMaximum); } private void prepareBrightnessAdjustmentSample() { if (!mBrightnessAdjustmentSamplePending) { mBrightnessAdjustmentSamplePending = true; mBrightnessAdjustmentSampleOldLux = mAmbientLuxValid ? mAmbientLux : -1; mBrightnessAdjustmentSampleOldBrightness = mScreenAutoBrightness; } else { mHandler.removeMessages(MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE); } mHandler.sendEmptyMessageDelayed(MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE, BRIGHTNESS_ADJUSTMENT_SAMPLE_DEBOUNCE_MILLIS); } private void cancelBrightnessAdjustmentSample() { if (mBrightnessAdjustmentSamplePending) { mBrightnessAdjustmentSamplePending = false; mHandler.removeMessages(MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE); } } private void collectBrightnessAdjustmentSample() { if (mBrightnessAdjustmentSamplePending) { mBrightnessAdjustmentSamplePending = false; if (mAmbientLuxValid && mScreenAutoBrightness >= 0) { if (DEBUG) { Slog.d(TAG, "Auto-brightness adjustment changed by user: " + "lux=" + mAmbientLux + ", " + "brightness=" + mScreenAutoBrightness + ", " + "ring=" + mAmbientLightRingBuffer); } EventLog.writeEvent(EventLogTags.AUTO_BRIGHTNESS_ADJ, mBrightnessAdjustmentSampleOldLux, mBrightnessAdjustmentSampleOldBrightness, mAmbientLux, mScreenAutoBrightness); } } } private final class AutomaticBrightnessHandler extends Handler { public AutomaticBrightnessHandler(Looper looper) { super(looper, null, true /*async*/); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_AMBIENT_LUX: updateAmbientLux(); break; case MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE: collectBrightnessAdjustmentSample(); break; case MSG_INVALIDATE_SHORT_TERM_MODEL: invalidateShortTermModel(); break; } } } private final SensorEventListener mLightSensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { if (mLightSensorEnabled) { final long time = SystemClock.uptimeMillis(); final float lux = event.values[0]; handleLightSensorEvent(time, lux); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // Not used. } }; /** Callbacks to request updates to the display's power state. */ interface Callbacks { void updateBrightness(); } /** * A ring buffer of ambient light measurements sorted by time. * * Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted * from oldest to newest. */ private static final class AmbientLightRingBuffer { // Proportional extra capacity of the buffer beyond the expected number of light samples // in the horizon private static final float BUFFER_SLACK = 1.5f; private float[] mRingLux; private long[] mRingTime; private int mCapacity; // The first valid element and the next open slot. // Note that if mCount is zero then there are no valid elements. private int mStart; private int mEnd; private int mCount; public AmbientLightRingBuffer(long lightSensorRate, int ambientLightHorizon) { mCapacity = (int) Math.ceil(ambientLightHorizon * BUFFER_SLACK / lightSensorRate); mRingLux = new float[mCapacity]; mRingTime = new long[mCapacity]; } public float getLux(int index) { return mRingLux[offsetOf(index)]; } public long getTime(int index) { return mRingTime[offsetOf(index)]; } public void push(long time, float lux) { int next = mEnd; if (mCount == mCapacity) { int newSize = mCapacity * 2; float[] newRingLux = new float[newSize]; long[] newRingTime = new long[newSize]; int length = mCapacity - mStart; System.arraycopy(mRingLux, mStart, newRingLux, 0, length); System.arraycopy(mRingTime, mStart, newRingTime, 0, length); if (mStart != 0) { System.arraycopy(mRingLux, 0, newRingLux, length, mStart); System.arraycopy(mRingTime, 0, newRingTime, length, mStart); } mRingLux = newRingLux; mRingTime = newRingTime; next = mCapacity; mCapacity = newSize; mStart = 0; } mRingTime[next] = time; mRingLux[next] = lux; mEnd = next + 1; if (mEnd == mCapacity) { mEnd = 0; } mCount++; } public void prune(long horizon) { if (mCount == 0) { return; } while (mCount > 1) { int next = mStart + 1; if (next >= mCapacity) { next -= mCapacity; } if (mRingTime[next] > horizon) { // Some light sensors only produce data upon a change in the ambient light // levels, so we need to consider the previous measurement as the ambient light // level for all points in time up until we receive a new measurement. Thus, we // always want to keep the youngest element that would be removed from the // buffer and just set its measurement time to the horizon time since at that // point it is the ambient light level, and to remove it would be to drop a // valid data point within our horizon. break; } mStart = next; mCount -= 1; } if (mRingTime[mStart] < horizon) { mRingTime[mStart] = horizon; } } public int size() { return mCount; } public void clear() { mStart = 0; mEnd = 0; mCount = 0; } @Override public String toString() { StringBuffer buf = new StringBuffer(); buf.append('['); for (int i = 0; i < mCount; i++) { final long next = i + 1 < mCount ? getTime(i + 1) : SystemClock.uptimeMillis(); if (i != 0) { buf.append(", "); } buf.append(getLux(i)); buf.append(" / "); buf.append(next - getTime(i)); buf.append("ms"); } buf.append(']'); return buf.toString(); } private int offsetOf(int index) { if (index >= mCount || index < 0) { throw new ArrayIndexOutOfBoundsException(index); } index += mStart; if (index >= mCapacity) { index -= mCapacity; } return index; } } }