blob: 7a87c3a82e43809cc1f1ff309275d94e31ddef3c [file] [log] [blame]
/*
* 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.internal.display;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.display.BrightnessInfo;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.MathUtils;
import android.util.Slog;
import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
/**
* BrightnessSynchronizer helps convert between the int (old) system and float
* (new) system for storing the brightness. It has methods to convert between the two and also
* observes for when one of the settings is changed and syncs this with the other.
*/
public class BrightnessSynchronizer {
private static final String TAG = "BrightnessSynchronizer";
private static final boolean DEBUG = false;
private static final Uri BRIGHTNESS_URI =
Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS);
private static final long WAIT_FOR_RESPONSE_MILLIS = 200;
private static final int MSG_RUN_UPDATE = 1;
// The tolerance within which we consider brightness values approximately equal to eachother.
public static final float EPSILON = 0.0001f;
private static int sBrightnessUpdateCount = 1;
private final Context mContext;
private final BrightnessSyncObserver mBrightnessSyncObserver;
private final Clock mClock;
private final Handler mHandler;
private DisplayManager mDisplayManager;
private int mLatestIntBrightness;
private float mLatestFloatBrightness;
private BrightnessUpdate mCurrentUpdate;
private BrightnessUpdate mPendingUpdate;
public BrightnessSynchronizer(Context context) {
this(context, Looper.getMainLooper(), SystemClock::uptimeMillis);
}
@VisibleForTesting
public BrightnessSynchronizer(Context context, Looper looper, Clock clock) {
mContext = context;
mClock = clock;
mBrightnessSyncObserver = new BrightnessSyncObserver();
mHandler = new BrightnessSynchronizerHandler(looper);
}
/**
* Starts brightnessSyncObserver to ensure that the float and int brightness values stay
* in sync.
* This also ensures that values are synchronized at system start up too.
* So we force an update to the int value, since float is the source of truth. Fallback to int
* value, if float is invalid. If both are invalid, use default float value from config.
*/
public void startSynchronizing() {
if (mDisplayManager == null) {
mDisplayManager = mContext.getSystemService(DisplayManager.class);
}
if (mBrightnessSyncObserver.isObserving()) {
Slog.wtf(TAG, "Brightness sync observer requesting synchronization a second time.");
return;
}
mLatestFloatBrightness = getScreenBrightnessFloat();
mLatestIntBrightness = getScreenBrightnessInt();
Slog.i(TAG, "Initial brightness readings: " + mLatestIntBrightness + "(int), "
+ mLatestFloatBrightness + "(float)");
if (!Float.isNaN(mLatestFloatBrightness)) {
mPendingUpdate = new BrightnessUpdate(BrightnessUpdate.TYPE_FLOAT,
mLatestFloatBrightness);
} else if (mLatestIntBrightness != PowerManager.BRIGHTNESS_INVALID) {
mPendingUpdate = new BrightnessUpdate(BrightnessUpdate.TYPE_INT,
mLatestIntBrightness);
} else {
final float defaultBrightness = mContext.getResources().getFloat(
com.android.internal.R.dimen.config_screenBrightnessSettingDefaultFloat);
mPendingUpdate = new BrightnessUpdate(BrightnessUpdate.TYPE_FLOAT, defaultBrightness);
Slog.i(TAG, "Setting initial brightness to default value of: " + defaultBrightness);
}
mBrightnessSyncObserver.startObserving(mHandler);
mHandler.sendEmptyMessageAtTime(MSG_RUN_UPDATE, mClock.uptimeMillis());
}
/**
* Prints data on dumpsys.
*/
public void dump(PrintWriter pw) {
pw.println("BrightnessSynchronizer");
pw.println(" mLatestIntBrightness=" + mLatestIntBrightness);
pw.println(" mLatestFloatBrightness=" + mLatestFloatBrightness);
pw.println(" mCurrentUpdate=" + mCurrentUpdate);
pw.println(" mPendingUpdate=" + mPendingUpdate);
}
/**
* Converts between the int brightness system and the float brightness system.
*/
public static float brightnessIntToFloat(int brightnessInt) {
if (brightnessInt == PowerManager.BRIGHTNESS_OFF) {
return PowerManager.BRIGHTNESS_OFF_FLOAT;
} else if (brightnessInt == PowerManager.BRIGHTNESS_INVALID) {
return PowerManager.BRIGHTNESS_INVALID_FLOAT;
} else {
final float minFloat = PowerManager.BRIGHTNESS_MIN;
final float maxFloat = PowerManager.BRIGHTNESS_MAX;
final float minInt = PowerManager.BRIGHTNESS_OFF + 1;
final float maxInt = PowerManager.BRIGHTNESS_ON;
return MathUtils.constrainedMap(minFloat, maxFloat, minInt, maxInt, brightnessInt);
}
}
/**
* Converts between the float brightness system and the int brightness system.
*/
public static int brightnessFloatToInt(float brightnessFloat) {
return Math.round(brightnessFloatToIntRange(brightnessFloat));
}
/**
* Translates specified value from the float brightness system to the int brightness system,
* given the min/max of each range. Accounts for special values such as OFF and invalid values.
* Value returned as a float primitive (to preserve precision), but is a value within the
* int-system range.
*/
public static float brightnessFloatToIntRange(float brightnessFloat) {
if (floatEquals(brightnessFloat, PowerManager.BRIGHTNESS_OFF_FLOAT)) {
return PowerManager.BRIGHTNESS_OFF;
} else if (Float.isNaN(brightnessFloat)) {
return PowerManager.BRIGHTNESS_INVALID;
} else {
final float minFloat = PowerManager.BRIGHTNESS_MIN;
final float maxFloat = PowerManager.BRIGHTNESS_MAX;
final float minInt = PowerManager.BRIGHTNESS_OFF + 1;
final float maxInt = PowerManager.BRIGHTNESS_ON;
return MathUtils.constrainedMap(minInt, maxInt, minFloat, maxFloat, brightnessFloat);
}
}
/**
* Consumes a brightness change event for the float-based brightness.
*
* @param brightness Float brightness.
*/
private void handleBrightnessChangeFloat(float brightness) {
mLatestFloatBrightness = brightness;
handleBrightnessChange(BrightnessUpdate.TYPE_FLOAT, brightness);
}
/**
* Consumes a brightness change event for the int-based brightness.
*
* @param brightness Int brightness.
*/
private void handleBrightnessChangeInt(int brightness) {
mLatestIntBrightness = brightness;
handleBrightnessChange(BrightnessUpdate.TYPE_INT, brightness);
}
/**
* Consumes a brightness change event.
*
* @param type Type of the brightness change (int/float)
* @param brightness brightness.
*/
private void handleBrightnessChange(int type, float brightness) {
boolean swallowUpdate = mCurrentUpdate != null
&& mCurrentUpdate.swallowUpdate(type, brightness);
BrightnessUpdate prevUpdate = null;
if (!swallowUpdate) {
prevUpdate = mPendingUpdate;
mPendingUpdate = new BrightnessUpdate(type, brightness);
}
runUpdate();
// If we created a new update and it is still pending after the update, add a log.
if (!swallowUpdate && mPendingUpdate != null) {
Slog.i(TAG, "New PendingUpdate: " + mPendingUpdate + ", prev=" + prevUpdate);
}
}
/**
* Runs updates for current and pending BrightnessUpdates.
*/
private void runUpdate() {
if (DEBUG) {
Slog.d(TAG, "Running update mCurrent=" + mCurrentUpdate
+ ", mPending=" + mPendingUpdate);
}
// do-while instead of while to allow mCurrentUpdate to get set if there's a pending update.
do {
if (mCurrentUpdate != null) {
mCurrentUpdate.update();
if (mCurrentUpdate.isRunning()) {
break; // current update is still running, nothing to do.
} else if (mCurrentUpdate.isCompleted()) {
if (mCurrentUpdate.madeUpdates()) {
Slog.i(TAG, "Completed Update: " + mCurrentUpdate);
}
mCurrentUpdate = null;
}
}
// No current update any more, lets start the next update if there is one.
if (mCurrentUpdate == null && mPendingUpdate != null) {
mCurrentUpdate = mPendingUpdate;
mPendingUpdate = null;
}
} while (mCurrentUpdate != null);
}
/**
* Gets the stored screen brightness float value from the display brightness setting.
* @return brightness
*/
private float getScreenBrightnessFloat() {
return mDisplayManager.getBrightness(Display.DEFAULT_DISPLAY);
}
/**
* Gets the stored screen brightness int from the system settings.
* @return brightness
*/
private int getScreenBrightnessInt() {
return Settings.System.getIntForUser(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS, PowerManager.BRIGHTNESS_INVALID,
UserHandle.USER_CURRENT);
}
/**
* Tests whether two brightness float values are within a small enough tolerance
* of each other.
* @param a first float to compare
* @param b second float to compare
* @return whether the two values are within a small enough tolerance value
*/
public static boolean floatEquals(float a, float b) {
if (a == b) {
return true;
} else if (Float.isNaN(a) && Float.isNaN(b)) {
return true;
} else if (Math.abs(a - b) < EPSILON) {
return true;
} else {
return false;
}
}
/**
* Converts between the int brightness setting and the float brightness system. The int
* brightness setting is between 0-255 and matches the brightness slider - e.g. 128 is 50% on
* the slider. Accounts for special values such as OFF and invalid values. Accounts for
* brightness limits; the maximum value here represents the max value allowed on the slider.
*/
@VisibleForTesting
@SuppressLint("AndroidFrameworkRequiresPermission")
public float brightnessIntSettingToFloat(int brightnessInt) {
if (brightnessInt == PowerManager.BRIGHTNESS_OFF) {
return PowerManager.BRIGHTNESS_OFF_FLOAT;
} else if (brightnessInt == PowerManager.BRIGHTNESS_INVALID) {
return PowerManager.BRIGHTNESS_INVALID_FLOAT;
} else {
final float minInt = PowerManager.BRIGHTNESS_OFF + 1;
final float maxInt = PowerManager.BRIGHTNESS_ON;
// Normalize to the range [0, 1]
float userPerceptionBrightness = MathUtils.norm(minInt, maxInt, brightnessInt);
// Convert from user-perception to linear scale
float linearBrightness = BrightnessUtils.convertGammaToLinear(userPerceptionBrightness);
// Interpolate to the range [0, currentlyAllowedMax]
final Display display = mContext.getDisplay();
if (display == null) {
return PowerManager.BRIGHTNESS_INVALID_FLOAT;
}
final BrightnessInfo info = display.getBrightnessInfo();
return MathUtils.lerp(info.brightnessMinimum, info.brightnessMaximum, linearBrightness);
}
}
/**
* Translates specified value from the float brightness system to the setting int brightness
* system. The value returned is between 0-255 and matches the brightness slider - e.g. 128 is
* 50% on the slider. Accounts for special values such as OFF and invalid values. Accounts for
* brightness limits; the maximum value here represents the max value currently allowed on
* the slider.
*/
@VisibleForTesting
@SuppressLint("AndroidFrameworkRequiresPermission")
public int brightnessFloatToIntSetting(float brightnessFloat) {
if (floatEquals(brightnessFloat, PowerManager.BRIGHTNESS_OFF_FLOAT)) {
return PowerManager.BRIGHTNESS_OFF;
} else if (Float.isNaN(brightnessFloat)) {
return PowerManager.BRIGHTNESS_INVALID;
} else {
// Normalize to the range [0, 1]
final Display display = mContext.getDisplay();
if (display == null) {
return PowerManager.BRIGHTNESS_INVALID;
}
final BrightnessInfo info = display.getBrightnessInfo();
float linearBrightness =
MathUtils.norm(info.brightnessMinimum, info.brightnessMaximum, brightnessFloat);
// Convert from linear to user-perception scale
float userPerceptionBrightness = BrightnessUtils.convertLinearToGamma(linearBrightness);
// Interpolate to the range [0, 255]
final float minInt = PowerManager.BRIGHTNESS_OFF + 1;
final float maxInt = PowerManager.BRIGHTNESS_ON;
float intBrightness = MathUtils.lerp(minInt, maxInt, userPerceptionBrightness);
return Math.round(intBrightness);
}
}
/**
* Encapsulates a brightness change event and contains logic for synchronizing the appropriate
* settings for the specified brightness change.
*/
@VisibleForTesting
public class BrightnessUpdate {
static final int TYPE_INT = 0x1;
static final int TYPE_FLOAT = 0x2;
private static final int STATE_NOT_STARTED = 1;
private static final int STATE_RUNNING = 2;
private static final int STATE_COMPLETED = 3;
private final int mSourceType;
private final float mBrightness;
private long mTimeUpdated;
private int mState;
private int mUpdatedTypes;
private int mConfirmedTypes;
private int mId;
BrightnessUpdate(int sourceType, float brightness) {
mId = sBrightnessUpdateCount++;
mSourceType = sourceType;
mBrightness = brightness;
mTimeUpdated = 0;
mUpdatedTypes = 0x0;
mConfirmedTypes = 0x0;
mState = STATE_NOT_STARTED;
}
@Override
public String toString() {
return "{[" + mId + "] " + toStringLabel(mSourceType, mBrightness)
+ ", mUpdatedTypes=" + mUpdatedTypes + ", mConfirmedTypes=" + mConfirmedTypes
+ ", mTimeUpdated=" + mTimeUpdated + "}";
}
/**
* Runs the synchronization process, moving forward through the internal state machine.
*/
void update() {
if (mState == STATE_NOT_STARTED) {
mState = STATE_RUNNING;
// check if we need to update int
int brightnessInt = getBrightnessAsInt();
if (mLatestIntBrightness != brightnessInt) {
Settings.System.putIntForUser(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS, brightnessInt,
UserHandle.USER_CURRENT);
mLatestIntBrightness = brightnessInt;
mUpdatedTypes |= TYPE_INT;
}
// check if we need to update float
float brightnessFloat = getBrightnessAsFloat();
if (!floatEquals(mLatestFloatBrightness, brightnessFloat)) {
mDisplayManager.setBrightness(Display.DEFAULT_DISPLAY, brightnessFloat);
mLatestFloatBrightness = brightnessFloat;
mUpdatedTypes |= TYPE_FLOAT;
}
// If we made updates, lets wait for responses.
if (mUpdatedTypes != 0x0) {
// Give some time for our updates to return a confirmation response. If they
// don't return by that time, MSG_RUN_UPDATE will get sent and we will stop
// listening for responses and mark this update as complete.
if (DEBUG) {
Slog.d(TAG, "Sending MSG_RUN_UPDATE for "
+ toStringLabel(mSourceType, mBrightness));
}
Slog.i(TAG, "[" + mId + "] New Update "
+ toStringLabel(mSourceType, mBrightness) + " set brightness values: "
+ toStringLabel(mUpdatedTypes & TYPE_FLOAT, brightnessFloat) + " "
+ toStringLabel(mUpdatedTypes & TYPE_INT, brightnessInt));
mHandler.sendEmptyMessageAtTime(MSG_RUN_UPDATE,
mClock.uptimeMillis() + WAIT_FOR_RESPONSE_MILLIS);
}
mTimeUpdated = mClock.uptimeMillis();
}
if (mState == STATE_RUNNING) {
// If we're not waiting on any more confirmations or the time has expired, move to
// completed state.
if (mConfirmedTypes == mUpdatedTypes
|| (mTimeUpdated + WAIT_FOR_RESPONSE_MILLIS) < mClock.uptimeMillis()) {
mState = STATE_COMPLETED;
}
}
}
/**
* Attempts to consume the specified brightness change if it is determined that the change
* is a notification of a change previously made by this class.
*
* @param type The type of change (int|float)
* @param brightness The brightness value.
* @return True if the change was caused by this class, thus swallowed.
*/
boolean swallowUpdate(int type, float brightness) {
if ((mUpdatedTypes & type) != type || (mConfirmedTypes & type) != 0x0) {
// It's either a type we didn't update, or one we've already confirmed.
return false;
}
final boolean floatUpdateConfirmed =
type == TYPE_FLOAT && floatEquals(getBrightnessAsFloat(), brightness);
final boolean intUpdateConfirmed =
type == TYPE_INT && getBrightnessAsInt() == (int) brightness;
if (floatUpdateConfirmed || intUpdateConfirmed) {
mConfirmedTypes |= type;
Slog.i(TAG, "Swallowing update of " + toStringLabel(type, brightness)
+ " by update: " + this);
return true;
}
return false;
}
boolean isRunning() {
return mState == STATE_RUNNING;
}
boolean isCompleted() {
return mState == STATE_COMPLETED;
}
boolean madeUpdates() {
return mUpdatedTypes != 0x0;
}
private int getBrightnessAsInt() {
if (mSourceType == TYPE_INT) {
return (int) mBrightness;
}
return brightnessFloatToIntSetting(mBrightness);
}
private float getBrightnessAsFloat() {
if (mSourceType == TYPE_FLOAT) {
return mBrightness;
}
return brightnessIntSettingToFloat((int) mBrightness);
}
private String toStringLabel(int type, float brightness) {
return (type == TYPE_INT) ? ((int) brightness) + "(i)"
: ((type == TYPE_FLOAT) ? brightness + "(f)"
: "");
}
}
/** Functional interface for providing time. */
@VisibleForTesting
public interface Clock {
/** @return system uptime in milliseconds. */
long uptimeMillis();
}
class BrightnessSynchronizerHandler extends Handler {
BrightnessSynchronizerHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_RUN_UPDATE:
if (DEBUG) {
Slog.d(TAG, "MSG_RUN_UPDATE");
}
runUpdate();
break;
default:
super.handleMessage(msg);
}
}
};
private class BrightnessSyncObserver {
private boolean mIsObserving;
private final DisplayListener mListener = new DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {}
@Override
public void onDisplayRemoved(int displayId) {}
@Override
public void onDisplayChanged(int displayId) {
handleBrightnessChangeFloat(getScreenBrightnessFloat());
}
};
private ContentObserver createBrightnessContentObserver(Handler handler) {
return new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
if (selfChange) {
return;
}
if (BRIGHTNESS_URI.equals(uri)) {
handleBrightnessChangeInt(getScreenBrightnessInt());
}
}
};
}
boolean isObserving() {
return mIsObserving;
}
void startObserving(Handler handler) {
final ContentResolver cr = mContext.getContentResolver();
cr.registerContentObserver(BRIGHTNESS_URI, false,
createBrightnessContentObserver(handler), UserHandle.USER_ALL);
mDisplayManager.registerDisplayListener(mListener, handler,
DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS);
mIsObserving = true;
}
}
}