/* * 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 static android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS; import android.annotation.RequiresPermission; 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. */ @android.ravenwood.annotation.RavenwoodKeepPartialClass 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; // Feature flag that will eventually be removed private final boolean mIntRangeUserPerceptionEnabled; public BrightnessSynchronizer(Context context, boolean intRangeUserPerceptionEnabled) { this(context, Looper.getMainLooper(), SystemClock::uptimeMillis, intRangeUserPerceptionEnabled); } @VisibleForTesting public BrightnessSynchronizer(Context context, Looper looper, Clock clock, boolean intRangeUserPerceptionEnabled) { mContext = context; mClock = clock; mBrightnessSyncObserver = new BrightnessSyncObserver(); mHandler = new BrightnessSynchronizerHandler(looper); mIntRangeUserPerceptionEnabled = intRangeUserPerceptionEnabled; } /** * 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); pw.println(" mIntRangeUserPerceptionEnabled=" + mIntRangeUserPerceptionEnabled); } /** * 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 */ @android.ravenwood.annotation.RavenwoodKeep 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. */ @RequiresPermission(CONTROL_DISPLAY_BRIGHTNESS) public static float brightnessIntSettingToFloat(Context context, 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 = context.getDisplay(); if (display == null) { return PowerManager.BRIGHTNESS_INVALID_FLOAT; } final BrightnessInfo info = display.getBrightnessInfo(); if (info == null) { return PowerManager.BRIGHTNESS_INVALID_FLOAT; } 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. */ @RequiresPermission(CONTROL_DISPLAY_BRIGHTNESS) public static int brightnessFloatToIntSetting(Context context, 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 = context.getDisplay(); if (display == null) { return PowerManager.BRIGHTNESS_INVALID; } final BrightnessInfo info = display.getBrightnessInfo(); if (info == null) { return PowerManager.BRIGHTNESS_INVALID; } 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; } @SuppressLint("AndroidFrameworkRequiresPermission") private int getBrightnessAsInt() { if (mSourceType == TYPE_INT) { return (int) mBrightness; } if (mIntRangeUserPerceptionEnabled) { return brightnessFloatToIntSetting(mContext, mBrightness); } else { return brightnessFloatToInt(mBrightness); } } @SuppressLint("AndroidFrameworkRequiresPermission") private float getBrightnessAsFloat() { if (mSourceType == TYPE_FLOAT) { return mBrightness; } if (mIntRangeUserPerceptionEnabled) { return brightnessIntSettingToFloat(mContext, (int) mBrightness); } else { return brightnessIntToFloat((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; } } }