blob: 2791f6a409be3be1add7803f1a9c48357da183a9 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.display;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.display.BrightnessInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.IThermalEventListener;
import android.os.IThermalService;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.Temperature;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.MathUtils;
import android.util.Slog;
import android.util.TimeUtils;
import android.view.SurfaceControlHdrLayerInfoListener;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData;
import com.android.server.display.DisplayManagerService.Clock;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.LinkedList;
/**
* Controls the status of high-brightness mode for devices that support it. This class assumes that
* an instance is always created even if a device does not support high-brightness mode (HBM); in
* the case where it is not supported, the majority of the logic is skipped. On devices that support
* HBM, we keep track of the ambient lux as well as historical usage of HBM to determine when HBM is
* allowed and not. This class's output is simply a brightness-range maximum value (queried via
* {@link #getCurrentBrightnessMax}) that changes depending on whether HBM is enabled or not.
*/
class HighBrightnessModeController {
private static final String TAG = "HighBrightnessModeController";
private static final boolean DEBUG = false;
private static final float HDR_PERCENT_OF_SCREEN_REQUIRED = 0.50f;
private final float mBrightnessMin;
private final float mBrightnessMax;
private final Handler mHandler;
private final Runnable mHbmChangeCallback;
private final Runnable mRecalcRunnable;
private final Clock mClock;
private final SkinThermalStatusObserver mSkinThermalStatusObserver;
private final Context mContext;
private final SettingsObserver mSettingsObserver;
private final Injector mInjector;
private HdrListener mHdrListener;
private HighBrightnessModeData mHbmData;
private IBinder mRegisteredDisplayToken;
private boolean mIsInAllowedAmbientRange = false;
private boolean mIsTimeAvailable = false;
private boolean mIsAutoBrightnessEnabled = false;
private float mBrightness;
private int mHbmMode = BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF;
private boolean mIsHdrLayerPresent = false;
private boolean mIsThermalStatusWithinLimit = true;
private boolean mIsBlockedByLowPowerMode = false;
private int mWidth;
private int mHeight;
private float mAmbientLux;
/**
* If HBM is currently running, this is the start time for the current HBM session.
*/
private long mRunningStartTimeMillis = -1;
/**
* List of previous HBM-events ordered from most recent to least recent.
* Meant to store only the events that fall into the most recent
* {@link mHbmData.timeWindowMillis}.
*/
private LinkedList<HbmEvent> mEvents = new LinkedList<>();
HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken,
float brightnessMin, float brightnessMax, HighBrightnessModeData hbmData,
Runnable hbmChangeCallback, Context context) {
this(new Injector(), handler, width, height, displayToken, brightnessMin, brightnessMax,
hbmData, hbmChangeCallback, context);
}
@VisibleForTesting
HighBrightnessModeController(Injector injector, Handler handler, int width, int height,
IBinder displayToken, float brightnessMin, float brightnessMax,
HighBrightnessModeData hbmData, Runnable hbmChangeCallback,
Context context) {
mInjector = injector;
mContext = context;
mClock = injector.getClock();
mHandler = handler;
mBrightness = brightnessMin;
mBrightnessMin = brightnessMin;
mBrightnessMax = brightnessMax;
mHbmChangeCallback = hbmChangeCallback;
mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler);
mSettingsObserver = new SettingsObserver(mHandler);
mRecalcRunnable = this::recalculateTimeAllowance;
mHdrListener = new HdrListener();
resetHbmData(width, height, displayToken, hbmData);
}
void setAutoBrightnessEnabled(boolean isEnabled) {
if (!deviceSupportsHbm() || isEnabled == mIsAutoBrightnessEnabled) {
return;
}
if (DEBUG) {
Slog.d(TAG, "setAutoBrightnessEnabled( " + isEnabled + " )");
}
mIsAutoBrightnessEnabled = isEnabled;
mIsInAllowedAmbientRange = false; // reset when auto-brightness switches
recalculateTimeAllowance();
}
float getCurrentBrightnessMin() {
return mBrightnessMin;
}
float getCurrentBrightnessMax() {
if (!deviceSupportsHbm() || isCurrentlyAllowed()) {
// Either the device doesn't support HBM, or HBM range is currently allowed (device
// it in a high-lux environment). In either case, return the highest brightness
// level supported by the device.
return mBrightnessMax;
} else {
// Hbm is not allowed, only allow up to the brightness where we
// transition to high brightness mode.
return mHbmData.transitionPoint;
}
}
float getNormalBrightnessMax() {
return deviceSupportsHbm() ? mHbmData.transitionPoint : mBrightnessMax;
}
float getHdrBrightnessValue() {
// For HDR brightness, we take the current brightness and scale it to the max. The reason
// we do this is because we want brightness to go to HBM max when it would normally go
// to normal max, meaning it should not wait to go to 10000 lux (or whatever the transition
// point happens to be) in order to go full HDR. Likewise, HDR on manual brightness should
// automatically scale the brightness without forcing the user to adjust to higher values.
return MathUtils.map(getCurrentBrightnessMin(), getCurrentBrightnessMax(),
mBrightnessMin, mBrightnessMax, mBrightness);
}
void onAmbientLuxChange(float ambientLux) {
mAmbientLux = ambientLux;
if (!deviceSupportsHbm() || !mIsAutoBrightnessEnabled) {
return;
}
final boolean isHighLux = (ambientLux >= mHbmData.minimumLux);
if (isHighLux != mIsInAllowedAmbientRange) {
mIsInAllowedAmbientRange = isHighLux;
recalculateTimeAllowance();
}
}
void onBrightnessChanged(float brightness) {
if (!deviceSupportsHbm()) {
return;
}
mBrightness = brightness;
// If we are starting or ending a high brightness mode session, store the current
// session in mRunningStartTimeMillis, or the old one in mEvents.
final boolean wasHbmDrainingAvailableTime = mRunningStartTimeMillis != -1;
final boolean shouldHbmDrainAvailableTime = mBrightness > mHbmData.transitionPoint
&& !mIsHdrLayerPresent;
if (wasHbmDrainingAvailableTime != shouldHbmDrainAvailableTime) {
final long currentTime = mClock.uptimeMillis();
if (shouldHbmDrainAvailableTime) {
mRunningStartTimeMillis = currentTime;
} else {
mEvents.addFirst(new HbmEvent(mRunningStartTimeMillis, currentTime));
mRunningStartTimeMillis = -1;
if (DEBUG) {
Slog.d(TAG, "New HBM event: " + mEvents.getFirst());
}
}
}
recalculateTimeAllowance();
}
int getHighBrightnessMode() {
return mHbmMode;
}
void stop() {
registerHdrListener(null /*displayToken*/);
mSkinThermalStatusObserver.stopObserving();
mSettingsObserver.stopObserving();
}
void resetHbmData(int width, int height, IBinder displayToken, HighBrightnessModeData hbmData) {
mWidth = width;
mHeight = height;
mHbmData = hbmData;
unregisterHdrListener();
mSkinThermalStatusObserver.stopObserving();
mSettingsObserver.stopObserving();
if (deviceSupportsHbm()) {
registerHdrListener(displayToken);
recalculateTimeAllowance();
if (mHbmData.thermalStatusLimit > PowerManager.THERMAL_STATUS_NONE) {
mIsThermalStatusWithinLimit = true;
mSkinThermalStatusObserver.startObserving();
}
if (!mHbmData.allowInLowPowerMode) {
mIsBlockedByLowPowerMode = false;
mSettingsObserver.startObserving();
}
}
}
void dump(PrintWriter pw) {
mHandler.runWithScissors(() -> dumpLocal(pw), 1000);
}
@VisibleForTesting
HdrListener getHdrListener() {
return mHdrListener;
}
private void dumpLocal(PrintWriter pw) {
pw.println("HighBrightnessModeController:");
pw.println(" mBrightness=" + mBrightness);
pw.println(" mCurrentMin=" + getCurrentBrightnessMin());
pw.println(" mCurrentMax=" + getCurrentBrightnessMax());
pw.println(" mHbmMode=" + BrightnessInfo.hbmToString(mHbmMode)
+ (mHbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR
? "(" + getHdrBrightnessValue() + ")" : ""));
pw.println(" mHbmData=" + mHbmData);
pw.println(" mAmbientLux=" + mAmbientLux
+ (mIsAutoBrightnessEnabled ? "" : " (old/invalid)"));
pw.println(" mIsInAllowedAmbientRange=" + mIsInAllowedAmbientRange);
pw.println(" mIsAutoBrightnessEnabled=" + mIsAutoBrightnessEnabled);
pw.println(" mIsHdrLayerPresent=" + mIsHdrLayerPresent);
pw.println(" mBrightnessMin=" + mBrightnessMin);
pw.println(" mBrightnessMax=" + mBrightnessMax);
pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis()));
pw.println(" mIsTimeAvailable= " + mIsTimeAvailable);
pw.println(" mRunningStartTimeMillis=" + TimeUtils.formatUptime(mRunningStartTimeMillis));
pw.println(" mIsThermalStatusWithinLimit=" + mIsThermalStatusWithinLimit);
pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode);
pw.println(" width*height=" + mWidth + "*" + mHeight);
pw.println(" mEvents=");
final long currentTime = mClock.uptimeMillis();
long lastStartTime = currentTime;
if (mRunningStartTimeMillis != -1) {
lastStartTime = dumpHbmEvent(pw, new HbmEvent(mRunningStartTimeMillis, currentTime));
}
for (HbmEvent event : mEvents) {
if (lastStartTime > event.endTimeMillis) {
pw.println(" event: [normal brightness]: "
+ TimeUtils.formatDuration(lastStartTime - event.endTimeMillis));
}
lastStartTime = dumpHbmEvent(pw, event);
}
mSkinThermalStatusObserver.dump(pw);
}
private long dumpHbmEvent(PrintWriter pw, HbmEvent event) {
final long duration = event.endTimeMillis - event.startTimeMillis;
pw.println(" event: ["
+ TimeUtils.formatUptime(event.startTimeMillis) + ", "
+ TimeUtils.formatUptime(event.endTimeMillis) + "] ("
+ TimeUtils.formatDuration(duration) + ")");
return event.startTimeMillis;
}
private boolean isCurrentlyAllowed() {
// Returns true if HBM is allowed (above the ambient lux threshold) and there's still
// time within the current window for additional HBM usage. We return false if there is an
// HDR layer because we don't want the brightness MAX to change for HDR, which has its
// brightness scaled in a different way than sunlight HBM that doesn't require changing
// the MAX. HDR also needs to work under manual brightness which never adjusts the
// brightness maximum; so we implement HDR-HBM in a way that doesn't adjust the max.
// See {@link #getHdrBrightnessValue}.
return !mIsHdrLayerPresent
&& (mIsAutoBrightnessEnabled && mIsTimeAvailable && mIsInAllowedAmbientRange
&& mIsThermalStatusWithinLimit && !mIsBlockedByLowPowerMode);
}
private boolean deviceSupportsHbm() {
return mHbmData != null;
}
private long calculateRemainingTime(long currentTime) {
if (!deviceSupportsHbm()) {
return 0;
}
long timeAlreadyUsed = 0;
// First, lets see how much time we've taken for any currently running
// session of HBM.
if (mRunningStartTimeMillis > 0) {
if (mRunningStartTimeMillis > currentTime) {
Slog.e(TAG, "Start time set to the future. curr: " + currentTime
+ ", start: " + mRunningStartTimeMillis);
mRunningStartTimeMillis = currentTime;
}
timeAlreadyUsed = currentTime - mRunningStartTimeMillis;
}
if (DEBUG) {
Slog.d(TAG, "Time already used after current session: " + timeAlreadyUsed);
}
// Next, lets iterate through the history of previous sessions and add those times.
final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis;
Iterator<HbmEvent> it = mEvents.iterator();
while (it.hasNext()) {
final HbmEvent event = it.next();
// If this event ended before the current Timing window, discard forever and ever.
if (event.endTimeMillis < windowstartTimeMillis) {
it.remove();
continue;
}
final long startTimeMillis = Math.max(event.startTimeMillis, windowstartTimeMillis);
timeAlreadyUsed += event.endTimeMillis - startTimeMillis;
}
if (DEBUG) {
Slog.d(TAG, "Time already used after all sessions: " + timeAlreadyUsed);
}
return Math.max(0, mHbmData.timeMaxMillis - timeAlreadyUsed);
}
/**
* Recalculates the allowable HBM time.
*/
private void recalculateTimeAllowance() {
final long currentTime = mClock.uptimeMillis();
final long remainingTime = calculateRemainingTime(currentTime);
// We allow HBM if there is more than the minimum required time available
// or if brightness is already in the high range, if there is any time left at all.
final boolean isAllowedWithoutRestrictions = remainingTime >= mHbmData.timeMinMillis;
final boolean isOnlyAllowedToStayOn = !isAllowedWithoutRestrictions
&& remainingTime > 0 && mBrightness > mHbmData.transitionPoint;
mIsTimeAvailable = isAllowedWithoutRestrictions || isOnlyAllowedToStayOn;
// Calculate the time at which we want to recalculate mIsTimeAvailable in case a lux or
// brightness change doesn't happen before then.
long nextTimeout = -1;
if (mBrightness > mHbmData.transitionPoint) {
// if we're in high-lux now, timeout when we run out of allowed time.
nextTimeout = currentTime + remainingTime;
} else if (!mIsTimeAvailable && mEvents.size() > 0) {
// If we are not allowed...timeout when the oldest event moved outside of the timing
// window by at least minTime. Basically, we're calculating the soonest time we can
// get {@code timeMinMillis} back to us.
final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis;
final HbmEvent lastEvent = mEvents.getLast();
final long startTimePlusMinMillis =
Math.max(windowstartTimeMillis, lastEvent.startTimeMillis)
+ mHbmData.timeMinMillis;
final long timeWhenMinIsGainedBack =
currentTime + (startTimePlusMinMillis - windowstartTimeMillis) - remainingTime;
nextTimeout = timeWhenMinIsGainedBack;
}
if (DEBUG) {
Slog.d(TAG, "HBM recalculated. IsAllowedWithoutRestrictions: "
+ isAllowedWithoutRestrictions
+ ", isOnlyAllowedToStayOn: " + isOnlyAllowedToStayOn
+ ", remainingAllowedTime: " + remainingTime
+ ", isLuxHigh: " + mIsInAllowedAmbientRange
+ ", isHBMCurrentlyAllowed: " + isCurrentlyAllowed()
+ ", isHdrLayerPresent: " + mIsHdrLayerPresent
+ ", isAutoBrightnessEnabled: " + mIsAutoBrightnessEnabled
+ ", mIsTimeAvailable: " + mIsTimeAvailable
+ ", mIsInAllowedAmbientRange: " + mIsInAllowedAmbientRange
+ ", mIsThermalStatusWithinLimit: " + mIsThermalStatusWithinLimit
+ ", mIsBlockedByLowPowerMode: " + mIsBlockedByLowPowerMode
+ ", mBrightness: " + mBrightness
+ ", RunningStartTimeMillis: " + mRunningStartTimeMillis
+ ", nextTimeout: " + (nextTimeout != -1 ? (nextTimeout - currentTime) : -1)
+ ", events: " + mEvents);
}
if (nextTimeout != -1) {
mHandler.removeCallbacks(mRecalcRunnable);
mHandler.postAtTime(mRecalcRunnable, nextTimeout + 1);
}
// Update the state of the world
updateHbmMode();
}
private void updateHbmMode() {
int newHbmMode = calculateHighBrightnessMode();
if (mHbmMode != newHbmMode) {
mHbmMode = newHbmMode;
mHbmChangeCallback.run();
}
}
private int calculateHighBrightnessMode() {
if (!deviceSupportsHbm()) {
return BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF;
} else if (mIsHdrLayerPresent) {
return BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR;
} else if (isCurrentlyAllowed()) {
return BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT;
}
return BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF;
}
private void registerHdrListener(IBinder displayToken) {
if (mRegisteredDisplayToken == displayToken) {
return;
}
unregisterHdrListener();
mRegisteredDisplayToken = displayToken;
if (mRegisteredDisplayToken != null) {
mHdrListener.register(mRegisteredDisplayToken);
}
}
private void unregisterHdrListener() {
if (mRegisteredDisplayToken != null) {
mHdrListener.unregister(mRegisteredDisplayToken);
mIsHdrLayerPresent = false;
}
}
/**
* Represents an event in which High Brightness Mode was enabled.
*/
private static class HbmEvent {
public long startTimeMillis;
public long endTimeMillis;
HbmEvent(long startTimeMillis, long endTimeMillis) {
this.startTimeMillis = startTimeMillis;
this.endTimeMillis = endTimeMillis;
}
@Override
public String toString() {
return "[Event: {" + startTimeMillis + ", " + endTimeMillis + "}, total: "
+ ((endTimeMillis - startTimeMillis) / 1000) + "]";
}
}
@VisibleForTesting
class HdrListener extends SurfaceControlHdrLayerInfoListener {
@Override
public void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers,
int maxW, int maxH, int flags) {
mHandler.post(() -> {
mIsHdrLayerPresent = numberOfHdrLayers > 0
&& (float) (maxW * maxH)
>= ((float) (mWidth * mHeight) * HDR_PERCENT_OF_SCREEN_REQUIRED);
// Calling the brightness update so that we can recalculate
// brightness with HDR in mind.
onBrightnessChanged(mBrightness);
});
}
}
private final class SkinThermalStatusObserver extends IThermalEventListener.Stub {
private final Injector mInjector;
private final Handler mHandler;
private IThermalService mThermalService;
private boolean mStarted;
SkinThermalStatusObserver(Injector injector, Handler handler) {
mInjector = injector;
mHandler = handler;
}
@Override
public void notifyThrottling(Temperature temp) {
if (DEBUG) {
Slog.d(TAG, "New thermal throttling status "
+ ", current thermal status = " + temp.getStatus()
+ ", threshold = " + mHbmData.thermalStatusLimit);
}
mHandler.post(() -> {
mIsThermalStatusWithinLimit = temp.getStatus() <= mHbmData.thermalStatusLimit;
// This recalculates HbmMode and runs mHbmChangeCallback if the mode has changed
updateHbmMode();
});
}
void startObserving() {
if (mStarted) {
if (DEBUG) {
Slog.d(TAG, "Thermal status observer already started");
}
return;
}
mThermalService = mInjector.getThermalService();
if (mThermalService == null) {
Slog.w(TAG, "Could not observe thermal status. Service not available");
return;
}
try {
// We get a callback immediately upon registering so there's no need to query
// for the current value.
mThermalService.registerThermalEventListenerWithType(this, Temperature.TYPE_SKIN);
mStarted = true;
} catch (RemoteException e) {
Slog.e(TAG, "Failed to register thermal status listener", e);
}
}
void stopObserving() {
mIsThermalStatusWithinLimit = true;
if (!mStarted) {
if (DEBUG) {
Slog.d(TAG, "Stop skipped because thermal status observer not started");
}
return;
}
try {
mThermalService.unregisterThermalEventListener(this);
mStarted = false;
} catch (RemoteException e) {
Slog.e(TAG, "Failed to unregister thermal status listener", e);
}
mThermalService = null;
}
void dump(PrintWriter writer) {
writer.println(" SkinThermalStatusObserver:");
writer.println(" mStarted: " + mStarted);
if (mThermalService != null) {
writer.println(" ThermalService available");
} else {
writer.println(" ThermalService not available");
}
}
}
private final class SettingsObserver extends ContentObserver {
private final Uri mLowPowerModeSetting = Settings.Global.getUriFor(
Settings.Global.LOW_POWER_MODE);
private boolean mStarted;
SettingsObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
updateLowPower();
}
void startObserving() {
if (!mStarted) {
mContext.getContentResolver().registerContentObserver(mLowPowerModeSetting,
false /*notifyForDescendants*/, this, UserHandle.USER_ALL);
mStarted = true;
updateLowPower();
}
}
void stopObserving() {
mIsBlockedByLowPowerMode = false;
if (mStarted) {
mContext.getContentResolver().unregisterContentObserver(this);
mStarted = false;
}
}
private void updateLowPower() {
final boolean isLowPowerMode = isLowPowerMode();
if (isLowPowerMode == mIsBlockedByLowPowerMode) {
return;
}
if (DEBUG) {
Slog.d(TAG, "Settings.Global.LOW_POWER_MODE enabled: " + isLowPowerMode);
}
mIsBlockedByLowPowerMode = isLowPowerMode;
// this recalculates HbmMode and runs mHbmChangeCallback if the mode has changed
updateHbmMode();
}
private boolean isLowPowerMode() {
return Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.LOW_POWER_MODE, 0) != 0;
}
}
public static class Injector {
public Clock getClock() {
return SystemClock::uptimeMillis;
}
public IThermalService getThermalService() {
return IThermalService.Stub.asInterface(
ServiceManager.getService(Context.THERMAL_SERVICE));
}
}
}