/*
|
* Copyright (C) 2018 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.power.batterysaver;
|
|
import static com.android.server.power.batterysaver.BatterySaverController.reasonToString;
|
|
import android.annotation.NonNull;
|
import android.annotation.StringRes;
|
import android.app.Notification;
|
import android.app.NotificationChannel;
|
import android.app.NotificationManager;
|
import android.app.PendingIntent;
|
import android.content.ContentResolver;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.content.res.Resources;
|
import android.database.ContentObserver;
|
import android.os.BatterySaverPolicyConfig;
|
import android.os.Handler;
|
import android.os.PowerManager;
|
import android.os.SystemClock;
|
import android.os.UserHandle;
|
import android.provider.Settings;
|
import android.util.Slog;
|
import android.util.proto.ProtoOutputStream;
|
|
import com.android.internal.R;
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.os.BackgroundThread;
|
import com.android.server.EventLogTags;
|
import com.android.server.power.BatterySaverStateMachineProto;
|
|
import java.io.PrintWriter;
|
import java.text.NumberFormat;
|
|
/**
|
* Decides when to enable / disable battery saver.
|
*
|
* IMPORTANT: This class shares the power manager lock, which is very low in the lock hierarchy.
|
* Do not call out with the lock held. (Settings provider is okay.)
|
*
|
* Test: atest com.android.server.power.batterysaver.BatterySaverStateMachineTest
|
*
|
* Current state machine. This can be visualized using Graphviz:
|
<pre>
|
|
digraph {
|
STATE_OFF
|
STATE_MANUAL_ON [label="STATE_MANUAL_ON\nTurned on manually by the user"]
|
STATE_AUTOMATIC_ON [label="STATE_AUTOMATIC_ON\nTurned on automatically by the system"]
|
STATE_OFF_AUTOMATIC_SNOOZED [
|
label="STATE_OFF_AUTOMATIC_SNOOZED\nTurned off manually by the user."
|
+ " The system should not turn it back on automatically."
|
]
|
STATE_PENDING_STICKY_ON [
|
label="STATE_PENDING_STICKY_ON\n"
|
+ " Turned on manually by the user and then plugged in. Will turn back on after unplug."
|
]
|
|
STATE_OFF -> STATE_MANUAL_ON [label="manual"]
|
STATE_OFF -> STATE_AUTOMATIC_ON [label="Auto on AND charge <= auto threshold"]
|
|
STATE_MANUAL_ON -> STATE_OFF [label="manual\nOR\nPlugged & sticky disabled"]
|
STATE_MANUAL_ON -> STATE_PENDING_STICKY_ON [label="Plugged & sticky enabled"]
|
|
STATE_PENDING_STICKY_ON -> STATE_MANUAL_ON [label="Unplugged & sticky enabled"]
|
STATE_PENDING_STICKY_ON -> STATE_OFF [
|
label="Sticky disabled\nOR\nSticky auto off enabled AND charge >= sticky auto off threshold"
|
]
|
|
STATE_AUTOMATIC_ON -> STATE_OFF [label="Plugged"]
|
STATE_AUTOMATIC_ON -> STATE_OFF_AUTOMATIC_SNOOZED [label="Manual"]
|
|
STATE_OFF_AUTOMATIC_SNOOZED -> STATE_OFF [label="Plug\nOR\nCharge > auto threshold"]
|
STATE_OFF_AUTOMATIC_SNOOZED -> STATE_MANUAL_ON [label="manual"]
|
|
</pre>
|
}
|
*/
|
public class BatterySaverStateMachine {
|
private static final String TAG = "BatterySaverStateMachine";
|
private static final String DYNAMIC_MODE_NOTIF_CHANNEL_ID = "dynamic_mode_notification";
|
private static final String BATTERY_SAVER_NOTIF_CHANNEL_ID = "battery_saver_channel";
|
private static final int DYNAMIC_MODE_NOTIFICATION_ID = 1992;
|
private static final int STICKY_AUTO_DISABLED_NOTIFICATION_ID = 1993;
|
private final Object mLock;
|
|
private static final boolean DEBUG = BatterySaverPolicy.DEBUG;
|
|
private static final long ADAPTIVE_CHANGE_TIMEOUT_MS = 24 * 60 * 60 * 1000L;
|
|
/** Turn off adaptive battery saver if the device has charged above this level. */
|
private static final int ADAPTIVE_AUTO_DISABLE_BATTERY_LEVEL = 80;
|
|
private static final int STATE_OFF = BatterySaverStateMachineProto.STATE_OFF;
|
|
/** Turned on manually by the user. */
|
private static final int STATE_MANUAL_ON = BatterySaverStateMachineProto.STATE_MANUAL_ON;
|
|
/** Turned on automatically by the system. */
|
private static final int STATE_AUTOMATIC_ON = BatterySaverStateMachineProto.STATE_AUTOMATIC_ON;
|
|
/** Turned off manually by the user. The system should not turn it back on automatically. */
|
private static final int STATE_OFF_AUTOMATIC_SNOOZED =
|
BatterySaverStateMachineProto.STATE_OFF_AUTOMATIC_SNOOZED;
|
|
/** Turned on manually by the user and then plugged in. Will turn back on after unplug. */
|
private static final int STATE_PENDING_STICKY_ON =
|
BatterySaverStateMachineProto.STATE_PENDING_STICKY_ON;
|
|
private final Context mContext;
|
private final BatterySaverController mBatterySaverController;
|
|
/** Whether the system has booted. */
|
@GuardedBy("mLock")
|
private boolean mBootCompleted;
|
|
/** Whether global settings have been loaded already. */
|
@GuardedBy("mLock")
|
private boolean mSettingsLoaded;
|
|
/** Whether the first battery status has arrived. */
|
@GuardedBy("mLock")
|
private boolean mBatteryStatusSet;
|
|
@GuardedBy("mLock")
|
private int mState;
|
|
/** Whether the device is connected to any power source. */
|
@GuardedBy("mLock")
|
private boolean mIsPowered;
|
|
/** Current battery level in %, 0-100. (Currently only used in dumpsys.) */
|
@GuardedBy("mLock")
|
private int mBatteryLevel;
|
|
/** Whether the battery level is considered to be "low" or not. */
|
@GuardedBy("mLock")
|
private boolean mIsBatteryLevelLow;
|
|
/** Previously known value of Settings.Global.LOW_POWER_MODE. */
|
@GuardedBy("mLock")
|
private boolean mSettingBatterySaverEnabled;
|
|
/** Previously known value of Settings.Global.LOW_POWER_MODE_STICKY. */
|
@GuardedBy("mLock")
|
private boolean mSettingBatterySaverEnabledSticky;
|
|
/** Config flag to track if battery saver's sticky behaviour is disabled. */
|
private final boolean mBatterySaverStickyBehaviourDisabled;
|
|
/**
|
* Whether or not to end sticky battery saver upon reaching a level specified by
|
* {@link #mSettingBatterySaverStickyAutoDisableThreshold}.
|
*/
|
@GuardedBy("mLock")
|
private boolean mSettingBatterySaverStickyAutoDisableEnabled;
|
|
/**
|
* The battery level at which to end sticky battery saver. Only useful if
|
* {@link #mSettingBatterySaverStickyAutoDisableEnabled} is {@code true}.
|
*/
|
@GuardedBy("mLock")
|
private int mSettingBatterySaverStickyAutoDisableThreshold;
|
|
/** Config flag to track default disable threshold for Dynamic Power Savings enabled battery
|
* saver. */
|
@GuardedBy("mLock")
|
private final int mDynamicPowerSavingsDefaultDisableThreshold;
|
|
/**
|
* Previously known value of Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL.
|
* (Currently only used in dumpsys.)
|
*/
|
@GuardedBy("mLock")
|
private int mSettingBatterySaverTriggerThreshold;
|
|
/** Previously known value of Settings.Global.AUTOMATIC_POWER_SAVE_MODE. */
|
@GuardedBy("mLock")
|
private int mSettingAutomaticBatterySaver;
|
|
/** When to disable battery saver again if it was enabled due to an external suggestion.
|
* Corresponds to Settings.Global.DYNAMIC_POWER_SAVINGS_DISABLE_THRESHOLD.
|
*/
|
@GuardedBy("mLock")
|
private int mDynamicPowerSavingsDisableThreshold;
|
|
/**
|
* Whether we've received a suggestion that battery saver should be on from an external app.
|
* Updates when Settings.Global.DYNAMIC_POWER_SAVINGS_ENABLED changes.
|
*/
|
@GuardedBy("mLock")
|
private boolean mDynamicPowerSavingsBatterySaver;
|
|
/**
|
* Last reason passed to {@link #enableBatterySaverLocked}.
|
*/
|
@GuardedBy("mLock")
|
private int mLastChangedIntReason;
|
|
/**
|
* Last reason passed to {@link #enableBatterySaverLocked}.
|
*/
|
@GuardedBy("mLock")
|
private String mLastChangedStrReason;
|
|
/**
|
* The last time adaptive battery saver was changed by an external service, using elapsed
|
* realtime as the timebase.
|
*/
|
@GuardedBy("mLock")
|
private long mLastAdaptiveBatterySaverChangedExternallyElapsed;
|
|
private final ContentObserver mSettingsObserver = new ContentObserver(null) {
|
@Override
|
public void onChange(boolean selfChange) {
|
synchronized (mLock) {
|
refreshSettingsLocked();
|
}
|
}
|
};
|
|
public BatterySaverStateMachine(Object lock,
|
Context context, BatterySaverController batterySaverController) {
|
mLock = lock;
|
mContext = context;
|
mBatterySaverController = batterySaverController;
|
mState = STATE_OFF;
|
|
mBatterySaverStickyBehaviourDisabled = mContext.getResources().getBoolean(
|
com.android.internal.R.bool.config_batterySaverStickyBehaviourDisabled);
|
mDynamicPowerSavingsDefaultDisableThreshold = mContext.getResources().getInteger(
|
com.android.internal.R.integer.config_dynamicPowerSavingsDefaultDisableThreshold);
|
}
|
|
/** @return true if the automatic percentage based mode should be used */
|
private boolean isAutomaticModeActiveLocked() {
|
return mSettingAutomaticBatterySaver == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE
|
&& mSettingBatterySaverTriggerThreshold > 0;
|
}
|
|
/**
|
* The returned value won't necessarily make sense if {@link #isAutomaticModeActiveLocked()}
|
* returns {@code false}.
|
*
|
* @return true if the battery level is below automatic's threshold.
|
*/
|
private boolean isInAutomaticLowZoneLocked() {
|
return mIsBatteryLevelLow;
|
}
|
|
/** @return true if the dynamic mode should be used */
|
private boolean isDynamicModeActiveLocked() {
|
return mSettingAutomaticBatterySaver == PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC
|
&& mDynamicPowerSavingsBatterySaver;
|
}
|
|
/**
|
* The returned value won't necessarily make sense if {@link #isDynamicModeActiveLocked()}
|
* returns {@code false}.
|
*
|
* @return true if the battery level is below dynamic's threshold.
|
*/
|
private boolean isInDynamicLowZoneLocked() {
|
return mBatteryLevel <= mDynamicPowerSavingsDisableThreshold;
|
}
|
|
/**
|
* {@link com.android.server.power.PowerManagerService} calls it when the system is booted.
|
*/
|
public void onBootCompleted() {
|
if (DEBUG) {
|
Slog.d(TAG, "onBootCompleted");
|
}
|
// Just booted. We don't want LOW_POWER_MODE to be persisted, so just always clear it.
|
putGlobalSetting(Settings.Global.LOW_POWER_MODE, 0);
|
|
// This is called with the power manager lock held. Don't do anything that may call to
|
// upper services. (e.g. don't call into AM directly)
|
// So use a BG thread.
|
runOnBgThread(() -> {
|
|
final ContentResolver cr = mContext.getContentResolver();
|
cr.registerContentObserver(Settings.Global.getUriFor(
|
Settings.Global.LOW_POWER_MODE),
|
false, mSettingsObserver, UserHandle.USER_SYSTEM);
|
cr.registerContentObserver(Settings.Global.getUriFor(
|
Settings.Global.LOW_POWER_MODE_STICKY),
|
false, mSettingsObserver, UserHandle.USER_SYSTEM);
|
cr.registerContentObserver(Settings.Global.getUriFor(
|
Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL),
|
false, mSettingsObserver, UserHandle.USER_SYSTEM);
|
cr.registerContentObserver(Settings.Global.getUriFor(
|
Settings.Global.AUTOMATIC_POWER_SAVE_MODE),
|
false, mSettingsObserver, UserHandle.USER_SYSTEM);
|
cr.registerContentObserver(Settings.Global.getUriFor(
|
Settings.Global.DYNAMIC_POWER_SAVINGS_ENABLED),
|
false, mSettingsObserver, UserHandle.USER_SYSTEM);
|
cr.registerContentObserver(Settings.Global.getUriFor(
|
Settings.Global.DYNAMIC_POWER_SAVINGS_DISABLE_THRESHOLD),
|
false, mSettingsObserver, UserHandle.USER_SYSTEM);
|
cr.registerContentObserver(Settings.Global.getUriFor(
|
Settings.Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED),
|
false, mSettingsObserver, UserHandle.USER_SYSTEM);
|
cr.registerContentObserver(Settings.Global.getUriFor(
|
Settings.Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_LEVEL),
|
false, mSettingsObserver, UserHandle.USER_SYSTEM);
|
|
|
synchronized (mLock) {
|
final boolean lowPowerModeEnabledSticky = getGlobalSetting(
|
Settings.Global.LOW_POWER_MODE_STICKY, 0) != 0;
|
|
if (lowPowerModeEnabledSticky) {
|
mState = STATE_PENDING_STICKY_ON;
|
}
|
|
mBootCompleted = true;
|
|
refreshSettingsLocked();
|
|
doAutoBatterySaverLocked();
|
}
|
});
|
}
|
|
/**
|
* Run a {@link Runnable} on a background handler.
|
*/
|
@VisibleForTesting
|
void runOnBgThread(Runnable r) {
|
BackgroundThread.getHandler().post(r);
|
}
|
|
/**
|
* Run a {@link Runnable} on a background handler, but lazily. If the same {@link Runnable} is
|
* already registered, it'll be first removed before being re-posted.
|
*/
|
@VisibleForTesting
|
void runOnBgThreadLazy(Runnable r, int delayMillis) {
|
final Handler h = BackgroundThread.getHandler();
|
h.removeCallbacks(r);
|
h.postDelayed(r, delayMillis);
|
}
|
|
@GuardedBy("mLock")
|
private void refreshSettingsLocked() {
|
final boolean lowPowerModeEnabled = getGlobalSetting(
|
Settings.Global.LOW_POWER_MODE, 0) != 0;
|
final boolean lowPowerModeEnabledSticky = getGlobalSetting(
|
Settings.Global.LOW_POWER_MODE_STICKY, 0) != 0;
|
final boolean dynamicPowerSavingsBatterySaver = getGlobalSetting(
|
Settings.Global.DYNAMIC_POWER_SAVINGS_ENABLED, 0) != 0;
|
final int lowPowerModeTriggerLevel = getGlobalSetting(
|
Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
|
final int automaticBatterySaverMode = getGlobalSetting(
|
Settings.Global.AUTOMATIC_POWER_SAVE_MODE,
|
PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
|
final int dynamicPowerSavingsDisableThreshold = getGlobalSetting(
|
Settings.Global.DYNAMIC_POWER_SAVINGS_DISABLE_THRESHOLD,
|
mDynamicPowerSavingsDefaultDisableThreshold);
|
final boolean isStickyAutoDisableEnabled = getGlobalSetting(
|
Settings.Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED, 1) != 0;
|
final int stickyAutoDisableThreshold = getGlobalSetting(
|
Settings.Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_LEVEL, 90);
|
|
setSettingsLocked(lowPowerModeEnabled, lowPowerModeEnabledSticky,
|
lowPowerModeTriggerLevel,
|
isStickyAutoDisableEnabled, stickyAutoDisableThreshold,
|
automaticBatterySaverMode,
|
dynamicPowerSavingsBatterySaver, dynamicPowerSavingsDisableThreshold);
|
}
|
|
/**
|
* {@link com.android.server.power.PowerManagerService} calls it when relevant global settings
|
* have changed.
|
*
|
* Note this will be called before {@link #onBootCompleted} too.
|
*/
|
@GuardedBy("mLock")
|
@VisibleForTesting
|
void setSettingsLocked(boolean batterySaverEnabled, boolean batterySaverEnabledSticky,
|
int batterySaverTriggerThreshold,
|
boolean isStickyAutoDisableEnabled, int stickyAutoDisableThreshold,
|
int automaticBatterySaver,
|
boolean dynamicPowerSavingsBatterySaver, int dynamicPowerSavingsDisableThreshold) {
|
if (DEBUG) {
|
Slog.d(TAG, "setSettings: enabled=" + batterySaverEnabled
|
+ " sticky=" + batterySaverEnabledSticky
|
+ " threshold=" + batterySaverTriggerThreshold
|
+ " stickyAutoDisableEnabled=" + isStickyAutoDisableEnabled
|
+ " stickyAutoDisableThreshold=" + stickyAutoDisableThreshold
|
+ " automaticBatterySaver=" + automaticBatterySaver
|
+ " dynamicPowerSavingsBatterySaver=" + dynamicPowerSavingsBatterySaver
|
+ " dynamicPowerSavingsDisableThreshold="
|
+ dynamicPowerSavingsDisableThreshold);
|
}
|
|
mSettingsLoaded = true;
|
|
// Set sensible limits.
|
stickyAutoDisableThreshold = Math.max(stickyAutoDisableThreshold,
|
batterySaverTriggerThreshold);
|
|
final boolean enabledChanged = mSettingBatterySaverEnabled != batterySaverEnabled;
|
final boolean stickyChanged =
|
mSettingBatterySaverEnabledSticky != batterySaverEnabledSticky;
|
final boolean thresholdChanged
|
= mSettingBatterySaverTriggerThreshold != batterySaverTriggerThreshold;
|
final boolean stickyAutoDisableEnabledChanged =
|
mSettingBatterySaverStickyAutoDisableEnabled != isStickyAutoDisableEnabled;
|
final boolean stickyAutoDisableThresholdChanged =
|
mSettingBatterySaverStickyAutoDisableThreshold != stickyAutoDisableThreshold;
|
final boolean automaticModeChanged = mSettingAutomaticBatterySaver != automaticBatterySaver;
|
final boolean dynamicPowerSavingsThresholdChanged =
|
mDynamicPowerSavingsDisableThreshold != dynamicPowerSavingsDisableThreshold;
|
final boolean dynamicPowerSavingsBatterySaverChanged =
|
mDynamicPowerSavingsBatterySaver != dynamicPowerSavingsBatterySaver;
|
|
if (!(enabledChanged || stickyChanged || thresholdChanged || automaticModeChanged
|
|| stickyAutoDisableEnabledChanged || stickyAutoDisableThresholdChanged
|
|| dynamicPowerSavingsThresholdChanged || dynamicPowerSavingsBatterySaverChanged)) {
|
return;
|
}
|
|
mSettingBatterySaverEnabled = batterySaverEnabled;
|
mSettingBatterySaverEnabledSticky = batterySaverEnabledSticky;
|
mSettingBatterySaverTriggerThreshold = batterySaverTriggerThreshold;
|
mSettingBatterySaverStickyAutoDisableEnabled = isStickyAutoDisableEnabled;
|
mSettingBatterySaverStickyAutoDisableThreshold = stickyAutoDisableThreshold;
|
mSettingAutomaticBatterySaver = automaticBatterySaver;
|
mDynamicPowerSavingsDisableThreshold = dynamicPowerSavingsDisableThreshold;
|
mDynamicPowerSavingsBatterySaver = dynamicPowerSavingsBatterySaver;
|
|
if (thresholdChanged) {
|
// To avoid spamming the event log, we throttle logging here.
|
runOnBgThreadLazy(mThresholdChangeLogger, 2000);
|
}
|
|
if (!mSettingBatterySaverStickyAutoDisableEnabled) {
|
hideStickyDisabledNotification();
|
}
|
|
if (enabledChanged) {
|
final String reason = batterySaverEnabled
|
? "Global.low_power changed to 1" : "Global.low_power changed to 0";
|
enableBatterySaverLocked(/*enable=*/ batterySaverEnabled, /*manual=*/ true,
|
BatterySaverController.REASON_SETTING_CHANGED, reason);
|
} else {
|
doAutoBatterySaverLocked();
|
}
|
}
|
|
private final Runnable mThresholdChangeLogger = () -> {
|
EventLogTags.writeBatterySaverSetting(mSettingBatterySaverTriggerThreshold);
|
};
|
|
/**
|
* {@link com.android.server.power.PowerManagerService} calls it when battery state changes.
|
*
|
* Note this may be called before {@link #onBootCompleted} too.
|
*/
|
public void setBatteryStatus(boolean newPowered, int newLevel, boolean newBatteryLevelLow) {
|
if (DEBUG) {
|
Slog.d(TAG, "setBatteryStatus: powered=" + newPowered + " level=" + newLevel
|
+ " low=" + newBatteryLevelLow);
|
}
|
synchronized (mLock) {
|
mBatteryStatusSet = true;
|
|
final boolean poweredChanged = mIsPowered != newPowered;
|
final boolean levelChanged = mBatteryLevel != newLevel;
|
final boolean lowChanged = mIsBatteryLevelLow != newBatteryLevelLow;
|
|
if (!(poweredChanged || levelChanged || lowChanged)) {
|
return;
|
}
|
|
mIsPowered = newPowered;
|
mBatteryLevel = newLevel;
|
mIsBatteryLevelLow = newBatteryLevelLow;
|
|
doAutoBatterySaverLocked();
|
}
|
}
|
|
/**
|
* Enable or disable the current adaptive battery saver policy. This may not change what's in
|
* effect if full battery saver is also enabled.
|
*/
|
public boolean setAdaptiveBatterySaverEnabled(boolean enabled) {
|
if (DEBUG) {
|
Slog.d(TAG, "setAdaptiveBatterySaverEnabled: enabled=" + enabled);
|
}
|
synchronized (mLock) {
|
mLastAdaptiveBatterySaverChangedExternallyElapsed = SystemClock.elapsedRealtime();
|
return mBatterySaverController.setAdaptivePolicyEnabledLocked(
|
enabled, BatterySaverController.REASON_ADAPTIVE_DYNAMIC_POWER_SAVINGS_CHANGED);
|
}
|
}
|
|
/**
|
* Change the adaptive battery saver policy.
|
*/
|
public boolean setAdaptiveBatterySaverPolicy(BatterySaverPolicyConfig config) {
|
if (DEBUG) {
|
Slog.d(TAG, "setAdaptiveBatterySaverPolicy: config=" + config);
|
}
|
|
synchronized (mLock) {
|
mLastAdaptiveBatterySaverChangedExternallyElapsed = SystemClock.elapsedRealtime();
|
return mBatterySaverController.setAdaptivePolicyLocked(config,
|
BatterySaverController.REASON_ADAPTIVE_DYNAMIC_POWER_SAVINGS_CHANGED);
|
}
|
}
|
|
/**
|
* Decide whether to auto-start / stop battery saver.
|
*/
|
@GuardedBy("mLock")
|
private void doAutoBatterySaverLocked() {
|
if (DEBUG) {
|
Slog.d(TAG, "doAutoBatterySaverLocked: mBootCompleted=" + mBootCompleted
|
+ " mSettingsLoaded=" + mSettingsLoaded
|
+ " mBatteryStatusSet=" + mBatteryStatusSet
|
+ " mState=" + mState
|
+ " mIsBatteryLevelLow=" + mIsBatteryLevelLow
|
+ " mIsPowered=" + mIsPowered
|
+ " mSettingAutomaticBatterySaver=" + mSettingAutomaticBatterySaver
|
+ " mSettingBatterySaverEnabledSticky=" + mSettingBatterySaverEnabledSticky
|
+ " mSettingBatterySaverStickyAutoDisableEnabled="
|
+ mSettingBatterySaverStickyAutoDisableEnabled);
|
}
|
if (!(mBootCompleted && mSettingsLoaded && mBatteryStatusSet)) {
|
return; // Not fully initialized yet.
|
}
|
|
updateStateLocked(false, false);
|
|
// Adaptive control.
|
if (SystemClock.elapsedRealtime() - mLastAdaptiveBatterySaverChangedExternallyElapsed
|
> ADAPTIVE_CHANGE_TIMEOUT_MS) {
|
mBatterySaverController.setAdaptivePolicyEnabledLocked(
|
false, BatterySaverController.REASON_TIMEOUT);
|
mBatterySaverController.resetAdaptivePolicyLocked(
|
BatterySaverController.REASON_TIMEOUT);
|
} else if (mIsPowered && mBatteryLevel >= ADAPTIVE_AUTO_DISABLE_BATTERY_LEVEL) {
|
mBatterySaverController.setAdaptivePolicyEnabledLocked(false,
|
BatterySaverController.REASON_PLUGGED_IN);
|
}
|
}
|
|
/**
|
* Update the state machine based on the current settings and battery/charge status.
|
*
|
* @param manual Whether the change was made by the user.
|
* @param enable Whether the user wants to turn battery saver on or off. Is only used if {@param
|
* manual} is true.
|
*/
|
@GuardedBy("mLock")
|
private void updateStateLocked(boolean manual, boolean enable) {
|
if (!manual && !(mBootCompleted && mSettingsLoaded && mBatteryStatusSet)) {
|
return; // Not fully initialized yet.
|
}
|
|
switch (mState) {
|
case STATE_OFF: {
|
if (!mIsPowered) {
|
if (manual) {
|
if (!enable) {
|
Slog.e(TAG, "Tried to disable BS when it's already OFF");
|
return;
|
}
|
enableBatterySaverLocked(/*enable*/ true, /*manual*/ true,
|
BatterySaverController.REASON_MANUAL_ON);
|
hideStickyDisabledNotification();
|
mState = STATE_MANUAL_ON;
|
} else if (isAutomaticModeActiveLocked() && isInAutomaticLowZoneLocked()) {
|
enableBatterySaverLocked(/*enable*/ true, /*manual*/ false,
|
BatterySaverController.REASON_PERCENTAGE_AUTOMATIC_ON);
|
hideStickyDisabledNotification();
|
mState = STATE_AUTOMATIC_ON;
|
} else if (isDynamicModeActiveLocked() && isInDynamicLowZoneLocked()) {
|
enableBatterySaverLocked(/*enable*/ true, /*manual*/ false,
|
BatterySaverController.REASON_DYNAMIC_POWER_SAVINGS_AUTOMATIC_ON);
|
hideStickyDisabledNotification();
|
mState = STATE_AUTOMATIC_ON;
|
}
|
}
|
break;
|
}
|
|
case STATE_MANUAL_ON: {
|
if (manual) {
|
if (enable) {
|
Slog.e(TAG, "Tried to enable BS when it's already MANUAL_ON");
|
return;
|
}
|
enableBatterySaverLocked(/*enable*/ false, /*manual*/ true,
|
BatterySaverController.REASON_MANUAL_OFF);
|
mState = STATE_OFF;
|
} else if (mIsPowered) {
|
enableBatterySaverLocked(/*enable*/ false, /*manual*/ false,
|
BatterySaverController.REASON_PLUGGED_IN);
|
if (mSettingBatterySaverEnabledSticky
|
&& !mBatterySaverStickyBehaviourDisabled) {
|
mState = STATE_PENDING_STICKY_ON;
|
} else {
|
mState = STATE_OFF;
|
}
|
}
|
break;
|
}
|
|
case STATE_AUTOMATIC_ON: {
|
if (mIsPowered) {
|
enableBatterySaverLocked(/*enable*/ false, /*manual*/ false,
|
BatterySaverController.REASON_PLUGGED_IN);
|
mState = STATE_OFF;
|
} else if (manual) {
|
if (enable) {
|
Slog.e(TAG, "Tried to enable BS when it's already AUTO_ON");
|
return;
|
}
|
enableBatterySaverLocked(/*enable*/ false, /*manual*/ true,
|
BatterySaverController.REASON_MANUAL_OFF);
|
// When battery saver is disabled manually (while battery saver is enabled)
|
// when the battery level is low, we "snooze" BS -- i.e. disable auto battery
|
// saver.
|
// We resume auto-BS once the battery level is not low, or the device is
|
// plugged in.
|
mState = STATE_OFF_AUTOMATIC_SNOOZED;
|
} else if (isAutomaticModeActiveLocked() && !isInAutomaticLowZoneLocked()) {
|
enableBatterySaverLocked(/*enable*/ false, /*manual*/ false,
|
BatterySaverController.REASON_PERCENTAGE_AUTOMATIC_OFF);
|
mState = STATE_OFF;
|
} else if (isDynamicModeActiveLocked() && !isInDynamicLowZoneLocked()) {
|
enableBatterySaverLocked(/*enable*/ false, /*manual*/ false,
|
BatterySaverController.REASON_DYNAMIC_POWER_SAVINGS_AUTOMATIC_OFF);
|
mState = STATE_OFF;
|
} else if (!isAutomaticModeActiveLocked() && !isDynamicModeActiveLocked()) {
|
enableBatterySaverLocked(/*enable*/ false, /*manual*/ false,
|
BatterySaverController.REASON_SETTING_CHANGED);
|
mState = STATE_OFF;
|
}
|
break;
|
}
|
|
case STATE_OFF_AUTOMATIC_SNOOZED: {
|
if (manual) {
|
if (!enable) {
|
Slog.e(TAG, "Tried to disable BS when it's already AUTO_SNOOZED");
|
return;
|
}
|
enableBatterySaverLocked(/*enable*/ true, /*manual*/ true,
|
BatterySaverController.REASON_MANUAL_ON);
|
mState = STATE_MANUAL_ON;
|
} else if (mIsPowered // Plugging in resets snooze.
|
|| (isAutomaticModeActiveLocked() && !isInAutomaticLowZoneLocked())
|
|| (isDynamicModeActiveLocked() && !isInDynamicLowZoneLocked())
|
|| (!isAutomaticModeActiveLocked() && !isDynamicModeActiveLocked())) {
|
mState = STATE_OFF;
|
}
|
break;
|
}
|
|
case STATE_PENDING_STICKY_ON: {
|
if (manual) {
|
// This shouldn't be possible. We'll only be in this state when the device is
|
// plugged in, so the user shouldn't be able to manually change state.
|
Slog.e(TAG, "Tried to manually change BS state from PENDING_STICKY_ON");
|
return;
|
}
|
final boolean shouldTurnOffSticky = mSettingBatterySaverStickyAutoDisableEnabled
|
&& mBatteryLevel >= mSettingBatterySaverStickyAutoDisableThreshold;
|
final boolean isStickyDisabled =
|
mBatterySaverStickyBehaviourDisabled || !mSettingBatterySaverEnabledSticky;
|
if (isStickyDisabled || shouldTurnOffSticky) {
|
mState = STATE_OFF;
|
setStickyActive(false);
|
triggerStickyDisabledNotification();
|
} else if (!mIsPowered) {
|
// Re-enable BS.
|
enableBatterySaverLocked(/*enable*/ true, /*manual*/ true,
|
BatterySaverController.REASON_STICKY_RESTORE);
|
mState = STATE_MANUAL_ON;
|
}
|
break;
|
}
|
|
default:
|
Slog.wtf(TAG, "Unknown state: " + mState);
|
break;
|
}
|
}
|
|
@VisibleForTesting
|
int getState() {
|
synchronized (mLock) {
|
return mState;
|
}
|
}
|
|
/**
|
* {@link com.android.server.power.PowerManagerService} calls it when
|
* {@link android.os.PowerManager#setPowerSaveModeEnabled} is called.
|
*
|
* Note this could? be called before {@link #onBootCompleted} too.
|
*/
|
public void setBatterySaverEnabledManually(boolean enabled) {
|
if (DEBUG) {
|
Slog.d(TAG, "setBatterySaverEnabledManually: enabled=" + enabled);
|
}
|
synchronized (mLock) {
|
updateStateLocked(true, enabled);
|
// TODO: maybe turn off adaptive if it's on and advertiseIsEnabled is true and
|
// enabled is false
|
}
|
}
|
|
@GuardedBy("mLock")
|
private void enableBatterySaverLocked(boolean enable, boolean manual, int intReason) {
|
enableBatterySaverLocked(enable, manual, intReason, reasonToString(intReason));
|
}
|
|
/**
|
* Actually enable / disable battery saver. Write the new state to the global settings
|
* and propagate it to {@link #mBatterySaverController}.
|
*/
|
@GuardedBy("mLock")
|
private void enableBatterySaverLocked(boolean enable, boolean manual, int intReason,
|
String strReason) {
|
if (DEBUG) {
|
Slog.d(TAG, "enableBatterySaver: enable=" + enable + " manual=" + manual
|
+ " reason=" + strReason + "(" + intReason + ")");
|
}
|
final boolean wasEnabled = mBatterySaverController.isFullEnabled();
|
|
if (wasEnabled == enable) {
|
if (DEBUG) {
|
Slog.d(TAG, "Already " + (enable ? "enabled" : "disabled"));
|
}
|
return;
|
}
|
if (enable && mIsPowered) {
|
if (DEBUG) Slog.d(TAG, "Can't enable: isPowered");
|
return;
|
}
|
mLastChangedIntReason = intReason;
|
mLastChangedStrReason = strReason;
|
|
mSettingBatterySaverEnabled = enable;
|
putGlobalSetting(Settings.Global.LOW_POWER_MODE, enable ? 1 : 0);
|
|
if (manual) {
|
setStickyActive(!mBatterySaverStickyBehaviourDisabled && enable);
|
}
|
mBatterySaverController.enableBatterySaver(enable, intReason);
|
|
// Handle triggering the notification to show/hide when appropriate
|
if (intReason == BatterySaverController.REASON_DYNAMIC_POWER_SAVINGS_AUTOMATIC_ON) {
|
runOnBgThread(this::triggerDynamicModeNotification);
|
} else if (!enable) {
|
runOnBgThread(this::hideDynamicModeNotification);
|
}
|
|
if (DEBUG) {
|
Slog.d(TAG, "Battery saver: Enabled=" + enable
|
+ " manual=" + manual
|
+ " reason=" + strReason + "(" + intReason + ")");
|
}
|
}
|
|
@VisibleForTesting
|
void triggerDynamicModeNotification() {
|
NotificationManager manager = mContext.getSystemService(NotificationManager.class);
|
ensureNotificationChannelExists(manager, DYNAMIC_MODE_NOTIF_CHANNEL_ID,
|
R.string.dynamic_mode_notification_channel_name);
|
|
manager.notifyAsUser(TAG, DYNAMIC_MODE_NOTIFICATION_ID,
|
buildNotification(DYNAMIC_MODE_NOTIF_CHANNEL_ID,
|
mContext.getResources().getString(R.string.dynamic_mode_notification_title),
|
R.string.dynamic_mode_notification_summary,
|
Intent.ACTION_POWER_USAGE_SUMMARY),
|
UserHandle.ALL);
|
}
|
|
@VisibleForTesting
|
void triggerStickyDisabledNotification() {
|
NotificationManager manager = mContext.getSystemService(NotificationManager.class);
|
ensureNotificationChannelExists(manager, BATTERY_SAVER_NOTIF_CHANNEL_ID,
|
R.string.battery_saver_notification_channel_name);
|
|
final String percentage = NumberFormat.getPercentInstance()
|
.format((double) mBatteryLevel / 100.0);
|
manager.notifyAsUser(TAG, STICKY_AUTO_DISABLED_NOTIFICATION_ID,
|
buildNotification(BATTERY_SAVER_NOTIF_CHANNEL_ID,
|
mContext.getResources().getString(
|
R.string.battery_saver_charged_notification_title, percentage),
|
R.string.battery_saver_off_notification_summary,
|
Settings.ACTION_BATTERY_SAVER_SETTINGS),
|
UserHandle.ALL);
|
}
|
|
private void ensureNotificationChannelExists(NotificationManager manager,
|
@NonNull String channelId, @StringRes int nameId) {
|
NotificationChannel channel = new NotificationChannel(
|
channelId, mContext.getText(nameId), NotificationManager.IMPORTANCE_DEFAULT);
|
channel.setSound(null, null);
|
channel.setBlockableSystem(true);
|
manager.createNotificationChannel(channel);
|
}
|
|
private Notification buildNotification(@NonNull String channelId, @NonNull String title,
|
@StringRes int summaryId, @NonNull String intentAction) {
|
Resources res = mContext.getResources();
|
Intent intent = new Intent(intentAction);
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
PendingIntent batterySaverIntent = PendingIntent.getActivity(
|
mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
final String summary = res.getString(summaryId);
|
|
return new Notification.Builder(mContext, channelId)
|
.setSmallIcon(R.drawable.ic_battery)
|
.setContentTitle(title)
|
.setContentText(summary)
|
.setContentIntent(batterySaverIntent)
|
.setStyle(new Notification.BigTextStyle().bigText(summary))
|
.setOnlyAlertOnce(true)
|
.setAutoCancel(true)
|
.build();
|
}
|
|
private void hideDynamicModeNotification() {
|
hideNotification(DYNAMIC_MODE_NOTIFICATION_ID);
|
}
|
|
private void hideStickyDisabledNotification() {
|
hideNotification(STICKY_AUTO_DISABLED_NOTIFICATION_ID);
|
}
|
|
private void hideNotification(int notificationId) {
|
NotificationManager manager = mContext.getSystemService(NotificationManager.class);
|
manager.cancel(notificationId);
|
}
|
|
private void setStickyActive(boolean active) {
|
mSettingBatterySaverEnabledSticky = active;
|
putGlobalSetting(Settings.Global.LOW_POWER_MODE_STICKY,
|
mSettingBatterySaverEnabledSticky ? 1 : 0);
|
}
|
|
@VisibleForTesting
|
protected void putGlobalSetting(String key, int value) {
|
Settings.Global.putInt(mContext.getContentResolver(), key, value);
|
}
|
|
@VisibleForTesting
|
protected int getGlobalSetting(String key, int defValue) {
|
return Settings.Global.getInt(mContext.getContentResolver(), key, defValue);
|
}
|
|
public void dump(PrintWriter pw) {
|
synchronized (mLock) {
|
pw.println();
|
pw.println("Battery saver state machine:");
|
|
pw.print(" Enabled=");
|
pw.println(mBatterySaverController.isEnabled());
|
pw.print(" full=");
|
pw.println(mBatterySaverController.isFullEnabled());
|
pw.print(" adaptive=");
|
pw.print(mBatterySaverController.isAdaptiveEnabled());
|
if (mBatterySaverController.isAdaptiveEnabled()) {
|
pw.print(" (advertise=");
|
pw.print(
|
mBatterySaverController.getBatterySaverPolicy().shouldAdvertiseIsEnabled());
|
pw.print(")");
|
}
|
pw.println();
|
pw.print(" mState=");
|
pw.println(mState);
|
|
pw.print(" mLastChangedIntReason=");
|
pw.println(mLastChangedIntReason);
|
pw.print(" mLastChangedStrReason=");
|
pw.println(mLastChangedStrReason);
|
|
pw.print(" mBootCompleted=");
|
pw.println(mBootCompleted);
|
pw.print(" mSettingsLoaded=");
|
pw.println(mSettingsLoaded);
|
pw.print(" mBatteryStatusSet=");
|
pw.println(mBatteryStatusSet);
|
|
pw.print(" mIsPowered=");
|
pw.println(mIsPowered);
|
pw.print(" mBatteryLevel=");
|
pw.println(mBatteryLevel);
|
pw.print(" mIsBatteryLevelLow=");
|
pw.println(mIsBatteryLevelLow);
|
|
pw.print(" mSettingBatterySaverEnabled=");
|
pw.println(mSettingBatterySaverEnabled);
|
pw.print(" mSettingBatterySaverEnabledSticky=");
|
pw.println(mSettingBatterySaverEnabledSticky);
|
pw.print(" mSettingBatterySaverStickyAutoDisableEnabled=");
|
pw.println(mSettingBatterySaverStickyAutoDisableEnabled);
|
pw.print(" mSettingBatterySaverStickyAutoDisableThreshold=");
|
pw.println(mSettingBatterySaverStickyAutoDisableThreshold);
|
pw.print(" mSettingBatterySaverTriggerThreshold=");
|
pw.println(mSettingBatterySaverTriggerThreshold);
|
pw.print(" mBatterySaverStickyBehaviourDisabled=");
|
pw.println(mBatterySaverStickyBehaviourDisabled);
|
|
pw.print(" mLastAdaptiveBatterySaverChangedExternallyElapsed=");
|
pw.println(mLastAdaptiveBatterySaverChangedExternallyElapsed);
|
}
|
}
|
|
public void dumpProto(ProtoOutputStream proto, long tag) {
|
synchronized (mLock) {
|
final long token = proto.start(tag);
|
|
proto.write(BatterySaverStateMachineProto.ENABLED,
|
mBatterySaverController.isEnabled());
|
proto.write(BatterySaverStateMachineProto.STATE, mState);
|
proto.write(BatterySaverStateMachineProto.IS_FULL_ENABLED,
|
mBatterySaverController.isFullEnabled());
|
proto.write(BatterySaverStateMachineProto.IS_ADAPTIVE_ENABLED,
|
mBatterySaverController.isAdaptiveEnabled());
|
proto.write(BatterySaverStateMachineProto.SHOULD_ADVERTISE_IS_ENABLED,
|
mBatterySaverController.getBatterySaverPolicy().shouldAdvertiseIsEnabled());
|
|
proto.write(BatterySaverStateMachineProto.BOOT_COMPLETED, mBootCompleted);
|
proto.write(BatterySaverStateMachineProto.SETTINGS_LOADED, mSettingsLoaded);
|
proto.write(BatterySaverStateMachineProto.BATTERY_STATUS_SET, mBatteryStatusSet);
|
|
|
proto.write(BatterySaverStateMachineProto.IS_POWERED, mIsPowered);
|
proto.write(BatterySaverStateMachineProto.BATTERY_LEVEL, mBatteryLevel);
|
proto.write(BatterySaverStateMachineProto.IS_BATTERY_LEVEL_LOW, mIsBatteryLevelLow);
|
|
proto.write(BatterySaverStateMachineProto.SETTING_BATTERY_SAVER_ENABLED,
|
mSettingBatterySaverEnabled);
|
proto.write(BatterySaverStateMachineProto.SETTING_BATTERY_SAVER_ENABLED_STICKY,
|
mSettingBatterySaverEnabledSticky);
|
proto.write(BatterySaverStateMachineProto.SETTING_BATTERY_SAVER_TRIGGER_THRESHOLD,
|
mSettingBatterySaverTriggerThreshold);
|
proto.write(
|
BatterySaverStateMachineProto.SETTING_BATTERY_SAVER_STICKY_AUTO_DISABLE_ENABLED,
|
mSettingBatterySaverStickyAutoDisableEnabled);
|
proto.write(
|
BatterySaverStateMachineProto
|
.SETTING_BATTERY_SAVER_STICKY_AUTO_DISABLE_THRESHOLD,
|
mSettingBatterySaverStickyAutoDisableThreshold);
|
|
proto.write(
|
BatterySaverStateMachineProto
|
.LAST_ADAPTIVE_BATTERY_SAVER_CHANGED_EXTERNALLY_ELAPSED,
|
mLastAdaptiveBatterySaverChangedExternallyElapsed);
|
|
proto.end(token);
|
}
|
}
|
}
|