blob: cfdcd636904bbdfc2f9868c841893ad452962272 [file] [log] [blame]
/*
* Copyright (C) 2022 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 static com.android.server.display.DisplayDeviceConfig.DEFAULT_ID;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.display.BrightnessInfo;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IThermalEventListener;
import android.os.IThermalService;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.Temperature;
import android.provider.DeviceConfig;
import android.provider.DeviceConfigInterface;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData;
import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executor;
/**
* This class monitors various conditions, such as skin temperature throttling status, and limits
* the allowed brightness range accordingly.
*/
class BrightnessThrottler {
private static final String TAG = "BrightnessThrottler";
private static final boolean DEBUG = false;
private static final int THROTTLING_INVALID = -1;
private final Injector mInjector;
private final Handler mHandler;
// We need a separate handler for unit testing. These two handlers are the same throughout the
// non-test code.
private final Handler mDeviceConfigHandler;
private final Runnable mThrottlingChangeCallback;
private final SkinThermalStatusObserver mSkinThermalStatusObserver;
private final DeviceConfigListener mDeviceConfigListener;
private final DeviceConfigInterface mDeviceConfig;
private int mThrottlingStatus;
// Maps the throttling ID to the data. Sourced from DisplayDeviceConfig.
@NonNull
private HashMap<String, ThermalBrightnessThrottlingData> mDdcThermalThrottlingDataMap;
// Current throttling data being used.
// Null if we do not support throttling.
@Nullable
private ThermalBrightnessThrottlingData mThermalThrottlingData;
private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason =
BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
private String mUniqueDisplayId;
// The most recent string that has been set from DeviceConfig
private String mThermalBrightnessThrottlingDataString;
// The brightness throttling configuration that should be used.
private String mThermalBrightnessThrottlingDataId;
// This is a collection of brightness throttling data that has been written as overrides from
// the DeviceConfig. This will always take priority over the display device config data.
// We need to store the data for every display device, so we do not need to update this each
// time the underlying display device changes.
// This map is indexed by uniqueDisplayId, to provide maps for throttlingId -> throttlingData.
// HashMap< uniqueDisplayId, HashMap< throttlingDataId, ThermalBrightnessThrottlingData >>
private final HashMap<String, HashMap<String, ThermalBrightnessThrottlingData>>
mThermalBrightnessThrottlingDataOverride = new HashMap<>(1);
BrightnessThrottler(Handler handler, Runnable throttlingChangeCallback, String uniqueDisplayId,
String throttlingDataId,
@NonNull HashMap<String, ThermalBrightnessThrottlingData>
thermalBrightnessThrottlingDataMap) {
this(new Injector(), handler, handler, throttlingChangeCallback,
uniqueDisplayId, throttlingDataId, thermalBrightnessThrottlingDataMap);
}
@VisibleForTesting
BrightnessThrottler(Injector injector, Handler handler, Handler deviceConfigHandler,
Runnable throttlingChangeCallback, String uniqueDisplayId, String throttlingDataId,
@NonNull HashMap<String, ThermalBrightnessThrottlingData>
thermalBrightnessThrottlingDataMap) {
mInjector = injector;
mHandler = handler;
mDeviceConfigHandler = deviceConfigHandler;
mDdcThermalThrottlingDataMap = thermalBrightnessThrottlingDataMap;
mThrottlingChangeCallback = throttlingChangeCallback;
mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler);
mUniqueDisplayId = uniqueDisplayId;
mDeviceConfig = injector.getDeviceConfig();
mDeviceConfigListener = new DeviceConfigListener();
mThermalBrightnessThrottlingDataId = throttlingDataId;
mDdcThermalThrottlingDataMap = thermalBrightnessThrottlingDataMap;
loadThermalBrightnessThrottlingDataFromDeviceConfig();
loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(mDdcThermalThrottlingDataMap,
mThermalBrightnessThrottlingDataId, mUniqueDisplayId);
}
boolean deviceSupportsThrottling() {
return mThermalThrottlingData != null;
}
float getBrightnessCap() {
return mBrightnessCap;
}
int getBrightnessMaxReason() {
return mBrightnessMaxReason;
}
boolean isThrottled() {
return mBrightnessMaxReason != BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
}
void stop() {
mSkinThermalStatusObserver.stopObserving();
mDeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigListener);
// We're asked to stop throttling, so reset brightness restrictions.
mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
mBrightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
// We set throttling status to an invalid value here so that we act on the first throttling
// value received from the thermal service after registration, even if that throttling value
// is THROTTLING_NONE.
mThrottlingStatus = THROTTLING_INVALID;
}
void loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
HashMap<String, ThermalBrightnessThrottlingData> ddcThrottlingDataMap,
String brightnessThrottlingDataId,
String uniqueDisplayId) {
mDdcThermalThrottlingDataMap = ddcThrottlingDataMap;
mThermalBrightnessThrottlingDataId = brightnessThrottlingDataId;
mUniqueDisplayId = uniqueDisplayId;
resetThermalThrottlingData();
}
private float verifyAndConstrainBrightnessCap(float brightness) {
if (brightness < PowerManager.BRIGHTNESS_MIN) {
Slog.e(TAG, "brightness " + brightness + " is lower than the minimum possible "
+ "brightness " + PowerManager.BRIGHTNESS_MIN);
brightness = PowerManager.BRIGHTNESS_MIN;
}
if (brightness > PowerManager.BRIGHTNESS_MAX) {
Slog.e(TAG, "brightness " + brightness + " is higher than the maximum possible "
+ "brightness " + PowerManager.BRIGHTNESS_MAX);
brightness = PowerManager.BRIGHTNESS_MAX;
}
return brightness;
}
private void thermalStatusChanged(@Temperature.ThrottlingStatus int newStatus) {
if (mThrottlingStatus != newStatus) {
mThrottlingStatus = newStatus;
updateThermalThrottling();
}
}
private void updateThermalThrottling() {
if (!deviceSupportsThrottling()) {
return;
}
float brightnessCap = PowerManager.BRIGHTNESS_MAX;
int brightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
if (mThrottlingStatus != THROTTLING_INVALID && mThermalThrottlingData != null) {
// Throttling levels are sorted by increasing severity
for (ThrottlingLevel level : mThermalThrottlingData.throttlingLevels) {
if (level.thermalStatus <= mThrottlingStatus) {
brightnessCap = level.brightness;
brightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL;
} else {
// Throttling levels that are greater than the current status are irrelevant
break;
}
}
}
if (mBrightnessCap != brightnessCap || mBrightnessMaxReason != brightnessMaxReason) {
mBrightnessCap = verifyAndConstrainBrightnessCap(brightnessCap);
mBrightnessMaxReason = brightnessMaxReason;
if (DEBUG) {
Slog.d(TAG, "State changed: mBrightnessCap = " + mBrightnessCap
+ ", mBrightnessMaxReason = "
+ BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason));
}
if (mThrottlingChangeCallback != null) {
mThrottlingChangeCallback.run();
}
}
}
void dump(PrintWriter pw) {
mHandler.runWithScissors(() -> dumpLocal(pw), 1000);
}
private void dumpLocal(PrintWriter pw) {
pw.println("BrightnessThrottler:");
pw.println(" mThermalBrightnessThrottlingDataId=" + mThermalBrightnessThrottlingDataId);
pw.println(" mThermalThrottlingData=" + mThermalThrottlingData);
pw.println(" mUniqueDisplayId=" + mUniqueDisplayId);
pw.println(" mThrottlingStatus=" + mThrottlingStatus);
pw.println(" mBrightnessCap=" + mBrightnessCap);
pw.println(" mBrightnessMaxReason=" +
BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason));
pw.println(" mDdcThermalThrottlingDataMap=" + mDdcThermalThrottlingDataMap);
pw.println(" mThermalBrightnessThrottlingDataOverride="
+ mThermalBrightnessThrottlingDataOverride);
pw.println(" mThermalBrightnessThrottlingDataString="
+ mThermalBrightnessThrottlingDataString);
mSkinThermalStatusObserver.dump(pw);
}
private String getThermalBrightnessThrottlingDataString() {
return mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA,
/* defaultValue= */ null);
}
// The brightness throttling data id may or may not be specified in the string that is passed
// in, if there is none specified, we assume it is for the default case. Each string passed in
// here must be for one display and one throttling id.
// 123,1,critical,0.8
// 456,2,moderate,0.9,critical,0.7
// 456,2,moderate,0.9,critical,0.7,default
// 456,2,moderate,0.9,critical,0.7,id_2
// displayId, number, <state, val> * number
// displayId, <number, <state, val> * number>, throttlingId
private boolean parseAndAddData(@NonNull String strArray,
@NonNull HashMap<String, HashMap<String, ThermalBrightnessThrottlingData>>
displayIdToThrottlingIdToBtd) {
boolean validConfig = true;
String[] items = strArray.split(",");
int i = 0;
try {
String uniqueDisplayId = items[i++];
// number of throttling points
int noOfThrottlingPoints = Integer.parseInt(items[i++]);
List<ThrottlingLevel> throttlingLevels = new ArrayList<>(noOfThrottlingPoints);
// throttling level and point
for (int j = 0; j < noOfThrottlingPoints; j++) {
String severity = items[i++];
int status = parseThermalStatus(severity);
float brightnessPoint = parseBrightness(items[i++]);
throttlingLevels.add(new ThrottlingLevel(status, brightnessPoint));
}
String throttlingDataId = (i < items.length) ? items[i++] : DEFAULT_ID;
ThermalBrightnessThrottlingData throttlingLevelsData =
DisplayDeviceConfig.ThermalBrightnessThrottlingData.create(throttlingLevels);
// Add throttlingLevelsData to inner map where necessary.
HashMap<String, ThermalBrightnessThrottlingData> throttlingMapForDisplay =
displayIdToThrottlingIdToBtd.get(uniqueDisplayId);
if (throttlingMapForDisplay == null) {
throttlingMapForDisplay = new HashMap<>();
throttlingMapForDisplay.put(throttlingDataId, throttlingLevelsData);
displayIdToThrottlingIdToBtd.put(uniqueDisplayId, throttlingMapForDisplay);
} else if (throttlingMapForDisplay.containsKey(throttlingDataId)) {
Slog.e(TAG, "Throttling data for display " + uniqueDisplayId
+ "contains duplicate throttling ids: '" + throttlingDataId + "'");
return false;
} else {
throttlingMapForDisplay.put(throttlingDataId, throttlingLevelsData);
}
} catch (NumberFormatException | IndexOutOfBoundsException
| UnknownThermalStatusException e) {
Slog.e(TAG, "Throttling data is invalid array: '" + strArray + "'", e);
validConfig = false;
}
if (i != items.length) {
validConfig = false;
}
return validConfig;
}
private void loadThermalBrightnessThrottlingDataFromDeviceConfig() {
HashMap<String, HashMap<String, ThermalBrightnessThrottlingData>> tempThrottlingData =
new HashMap<>(1);
mThermalBrightnessThrottlingDataString = getThermalBrightnessThrottlingDataString();
boolean validConfig = true;
mThermalBrightnessThrottlingDataOverride.clear();
if (mThermalBrightnessThrottlingDataString != null) {
String[] throttlingDataSplits = mThermalBrightnessThrottlingDataString.split(";");
for (String s : throttlingDataSplits) {
if (!parseAndAddData(s, tempThrottlingData)) {
validConfig = false;
break;
}
}
if (validConfig) {
mThermalBrightnessThrottlingDataOverride.putAll(tempThrottlingData);
tempThrottlingData.clear();
}
} else {
Slog.w(TAG, "DeviceConfig ThermalBrightnessThrottlingData is null");
}
}
private void resetThermalThrottlingData() {
stop();
mDeviceConfigListener.startListening();
// Get throttling data for this id, if it exists
mThermalThrottlingData = getConfigFromId(mThermalBrightnessThrottlingDataId);
// Fallback to default id otherwise.
if (!DEFAULT_ID.equals(mThermalBrightnessThrottlingDataId)
&& mThermalThrottlingData == null) {
mThermalThrottlingData = getConfigFromId(DEFAULT_ID);
Slog.d(TAG, "Falling back to default throttling Id");
}
if (deviceSupportsThrottling()) {
mSkinThermalStatusObserver.startObserving();
}
}
private ThermalBrightnessThrottlingData getConfigFromId(String id) {
ThermalBrightnessThrottlingData returnValue;
// Fallback pattern for fetching correct throttling data for this display and id.
// 1) throttling data from device config for this throttling data id
returnValue = mThermalBrightnessThrottlingDataOverride.get(mUniqueDisplayId) == null
? null
: mThermalBrightnessThrottlingDataOverride.get(mUniqueDisplayId).get(id);
// 2) throttling data from ddc for this throttling data id
returnValue = returnValue == null
? mDdcThermalThrottlingDataMap.get(id)
: returnValue;
return returnValue;
}
/**
* Listens to config data change and updates the brightness throttling data using
* DisplayManager#KEY_BRIGHTNESS_THROTTLING_DATA.
* The format should be a string similar to: "local:4619827677550801152,2,moderate,0.5,severe,
* 0.379518072;local:4619827677550801151,1,moderate,0.75"
* In this order:
* <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>][,throttlingId]?
* Where [<severity as string>,<brightness cap>] is repeated for each throttling level, and the
* entirety is repeated for each display & throttling data id, separated by a semicolon.
*/
public class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
public Executor mExecutor = new HandlerExecutor(mDeviceConfigHandler);
public void startListening() {
mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
mExecutor, this);
}
@Override
public void onPropertiesChanged(DeviceConfig.Properties properties) {
loadThermalBrightnessThrottlingDataFromDeviceConfig();
resetThermalThrottlingData();
}
}
private float parseBrightness(String intVal) throws NumberFormatException {
float value = Float.parseFloat(intVal);
if (value < PowerManager.BRIGHTNESS_MIN || value > PowerManager.BRIGHTNESS_MAX) {
throw new NumberFormatException("Brightness constraint value out of bounds.");
}
return value;
}
@PowerManager.ThermalStatus private int parseThermalStatus(@NonNull String value)
throws UnknownThermalStatusException {
switch (value) {
case "none":
return PowerManager.THERMAL_STATUS_NONE;
case "light":
return PowerManager.THERMAL_STATUS_LIGHT;
case "moderate":
return PowerManager.THERMAL_STATUS_MODERATE;
case "severe":
return PowerManager.THERMAL_STATUS_SEVERE;
case "critical":
return PowerManager.THERMAL_STATUS_CRITICAL;
case "emergency":
return PowerManager.THERMAL_STATUS_EMERGENCY;
case "shutdown":
return PowerManager.THERMAL_STATUS_SHUTDOWN;
default:
throw new UnknownThermalStatusException("Invalid Thermal Status: " + value);
}
}
private static class UnknownThermalStatusException extends Exception {
UnknownThermalStatusException(String message) {
super(message);
}
}
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 = " + temp.getStatus());
}
mHandler.post(() -> {
final @Temperature.ThrottlingStatus int status = temp.getStatus();
thermalStatusChanged(status);
});
}
void startObserving() {
if (mStarted) {
if (DEBUG) {
Slog.d(TAG, "Thermal status observer already started");
}
return;
}
mThermalService = mInjector.getThermalService();
if (mThermalService == null) {
Slog.e(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() {
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");
}
}
}
public static class Injector {
public IThermalService getThermalService() {
return IThermalService.Stub.asInterface(
ServiceManager.getService(Context.THERMAL_SERVICE));
}
@NonNull
public DeviceConfigInterface getDeviceConfig() {
return DeviceConfigInterface.REAL;
}
}
}