Sticky battery saver

- When battery saver is enabled manually (i.e. via PM.setPowerSaveMode()),
it'll stick, and we'll re-enable battery saver even after a reboot
or a charge.

- Extracted all battery saver state transition logic into a separate
class.

Fix: 75033216
Bug: 74120126
Test: Manual test with "dumpsys battery set ...."
Test: atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java
Change-Id: If020cd48f341b339783fe09dd35bc7199e737a52
Test: dumpsys power
Test: incident_report power
Test: atest CtsBatterySavingTestCases
diff --git a/api/test-current.txt b/api/test-current.txt
index dd16771..81b0662 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -584,6 +584,7 @@
     field public static final java.lang.String HIDDEN_API_BLACKLIST_EXEMPTIONS = "hidden_api_blacklist_exemptions";
     field public static final java.lang.String LOCATION_GLOBAL_KILL_SWITCH = "location_global_kill_switch";
     field public static final java.lang.String LOW_POWER_MODE = "low_power";
+    field public static final java.lang.String LOW_POWER_MODE_STICKY = "low_power_sticky";
     field public static final java.lang.String USE_OPEN_WIFI_PACKAGE = "use_open_wifi_package";
   }
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 18fd67e..12f03be 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11004,13 +11004,21 @@
         public static final String SHOW_PROCESSES = "show_processes";
 
         /**
-         * If 1 low power mode is enabled.
+         * If 1 low power mode (aka battery saver) is enabled.
          * @hide
          */
         @TestApi
         public static final String LOW_POWER_MODE = "low_power";
 
         /**
+         * If 1, battery saver ({@link #LOW_POWER_MODE}) will be re-activated after the device
+         * is unplugged from a charger or rebooted.
+         * @hide
+         */
+        @TestApi
+        public static final String LOW_POWER_MODE_STICKY = "low_power_sticky";
+
+        /**
          * Battery level [1-100] at which low power mode automatically turns on.
          * If 0, it will not automatically turn on.
          * @hide
diff --git a/core/proto/android/server/powermanagerservice.proto b/core/proto/android/server/powermanagerservice.proto
index c58de56..eb60942 100644
--- a/core/proto/android/server/powermanagerservice.proto
+++ b/core/proto/android/server/powermanagerservice.proto
@@ -118,61 +118,60 @@
     // True if the sandman has just been summoned for the first time since entering
     // the dreaming or dozing state.  Indicates whether a new dream should begin.
     optional bool is_sandman_summoned = 23;
-    // If true, the device is in low power mode.
-    optional bool is_low_power_mode_enabled = 24;
     // True if the battery level is currently considered low.
-    optional bool is_battery_level_low = 25;
+    optional bool is_battery_level_low = 24;
     // True if we are currently in light device idle mode.
-    optional bool is_light_device_idle_mode = 26;
+    optional bool is_light_device_idle_mode = 25;
     // True if we are currently in device idle mode.
-    optional bool is_device_idle_mode = 27;
+    optional bool is_device_idle_mode = 26;
     // Set of app ids that we will always respect the wake locks for.
-    repeated int32 device_idle_whitelist = 28;
+    repeated int32 device_idle_whitelist = 27;
     // Set of app ids that are temporarily allowed to acquire wakelocks due to
     // high-pri message
-    repeated int32 device_idle_temp_whitelist = 29;
+    repeated int32 device_idle_temp_whitelist = 28;
     // Timestamp of the last time the device was awoken.
-    optional int64 last_wake_time_ms = 30;
+    optional int64 last_wake_time_ms = 29;
     // Timestamp of the last time the device was put to sleep.
-    optional int64 last_sleep_time_ms = 31;
+    optional int64 last_sleep_time_ms = 30;
     // Timestamp of the last call to user activity.
-    optional int64 last_user_activity_time_ms = 32;
-    optional int64 last_user_activity_time_no_change_lights_ms = 33;
+    optional int64 last_user_activity_time_ms = 31;
+    optional int64 last_user_activity_time_no_change_lights_ms = 32;
     // Timestamp of last interactive power hint.
-    optional int64 last_interactive_power_hint_time_ms = 34;
+    optional int64 last_interactive_power_hint_time_ms = 33;
     // Timestamp of the last screen brightness boost.
-    optional int64 last_screen_brightness_boost_time_ms = 35;
+    optional int64 last_screen_brightness_boost_time_ms = 34;
     // True if screen brightness boost is in progress.
-    optional bool is_screen_brightness_boost_in_progress = 36;
+    optional bool is_screen_brightness_boost_in_progress = 35;
     // True if the display power state has been fully applied, which means the
     // display is actually on or actually off or whatever was requested.
-    optional bool is_display_ready = 37;
+    optional bool is_display_ready = 36;
     // True if the wake lock suspend blocker has been acquired.
-    optional bool is_holding_wake_lock_suspend_blocker = 38;
+    optional bool is_holding_wake_lock_suspend_blocker = 37;
     // The suspend blocker used to keep the CPU alive when the display is on, the
     // display is getting ready or there is user activity (in which case the
     // display must be on).
-    optional bool is_holding_display_suspend_blocker = 39;
+    optional bool is_holding_display_suspend_blocker = 38;
     // Settings and configuration
-    optional PowerServiceSettingsAndConfigurationDumpProto settings_and_configuration = 40;
+    optional PowerServiceSettingsAndConfigurationDumpProto settings_and_configuration = 39;
     // Sleep timeout in ms. This can be -1.
-    optional sint32 sleep_timeout_ms = 41;
+    optional sint32 sleep_timeout_ms = 40;
     // Screen off timeout in ms
-    optional int32 screen_off_timeout_ms = 42;
+    optional int32 screen_off_timeout_ms = 41;
     // Screen dim duration in ms
-    optional int32 screen_dim_duration_ms = 43;
+    optional int32 screen_dim_duration_ms = 42;
     // We are currently in the middle of a batch change of uids.
-    optional bool are_uids_changing = 44;
+    optional bool are_uids_changing = 43;
     // Some uids have actually changed while mUidsChanging was true.
-    optional bool are_uids_changed = 45;
+    optional bool are_uids_changed = 44;
     // List of UIDs and their states
-    repeated UidStateProto uid_states = 46;
-    optional .android.os.LooperProto looper = 47;
+    repeated UidStateProto uid_states = 45;
+    optional .android.os.LooperProto looper = 46;
     // List of all wake locks acquired by applications.
-    repeated WakeLockProto wake_locks = 48;
+    repeated WakeLockProto wake_locks = 47;
     // List of all suspend blockers.
-    repeated SuspendBlockerProto suspend_blockers = 49;
-    optional WirelessChargerDetectorProto wireless_charger_detector = 50;
+    repeated SuspendBlockerProto suspend_blockers = 48;
+    optional WirelessChargerDetectorProto wireless_charger_detector = 49;
+    optional BatterySaverStateMachineProto battery_saver_state_machine = 50;
 }
 
 // A com.android.server.power.PowerManagerService.SuspendBlockerImpl object.
@@ -270,51 +269,80 @@
     optional bool are_dreams_activate_on_dock_setting = 17;
     // True if doze should not be started until after the screen off transition.
     optional bool is_doze_after_screen_off_config = 18;
-    // If true, the device is in low power mode.
-    optional bool is_low_power_mode_setting = 19;
-    // Current state of whether the settings are allowing auto low power mode.
-    optional bool is_auto_low_power_mode_configured = 20;
-    // The user turned off low power mode below the trigger level
-    optional bool is_auto_low_power_mode_snoozing = 21;
     // The minimum screen off timeout, in milliseconds.
-    optional int32 minimum_screen_off_timeout_config_ms = 22;
+    optional int32 minimum_screen_off_timeout_config_ms = 19;
     // The screen dim duration, in milliseconds.
-    optional int32 maximum_screen_dim_duration_config_ms = 23;
+    optional int32 maximum_screen_dim_duration_config_ms = 20;
     // The maximum screen dim time expressed as a ratio relative to the screen off timeout.
-    optional float maximum_screen_dim_ratio_config = 24;
+    optional float maximum_screen_dim_ratio_config = 21;
     // The screen off timeout setting value in milliseconds.
-    optional int32 screen_off_timeout_setting_ms = 25;
+    optional int32 screen_off_timeout_setting_ms = 22;
     // The sleep timeout setting value in milliseconds. Default value is -1.
-    optional sint32 sleep_timeout_setting_ms = 26;
+    optional sint32 sleep_timeout_setting_ms = 23;
     // The maximum allowable screen off timeout according to the device administration policy.
-    optional int32 maximum_screen_off_timeout_from_device_admin_ms = 27;
-    optional bool is_maximum_screen_off_timeout_from_device_admin_enforced_locked = 28;
+    optional int32 maximum_screen_off_timeout_from_device_admin_ms = 24;
+    optional bool is_maximum_screen_off_timeout_from_device_admin_enforced_locked = 25;
     // The stay on while plugged in setting.
     // A set of battery conditions under which to make the screen stay on.
-    optional StayOnWhilePluggedInProto stay_on_while_plugged_in = 29;
+    optional StayOnWhilePluggedInProto stay_on_while_plugged_in = 26;
     // The screen brightness mode.
-    optional .android.providers.settings.SettingsProto.ScreenBrightnessMode screen_brightness_mode_setting = 30;
+    optional .android.providers.settings.SettingsProto.ScreenBrightnessMode screen_brightness_mode_setting = 27;
     // The screen brightness setting override from the window manager
     // to allow the current foreground activity to override the brightness.
     // Use -1 to disable.
-    optional sint32 screen_brightness_override_from_window_manager = 31;
+    optional sint32 screen_brightness_override_from_window_manager = 28;
     // The user activity timeout override from the window manager
     // to allow the current foreground activity to override the user activity
     // timeout. Use -1 to disable.
-    optional sint64 user_activity_timeout_override_from_window_manager_ms = 32;
+    optional sint64 user_activity_timeout_override_from_window_manager_ms = 29;
     // The window manager has determined the user to be inactive via other means.
     // Set this to false to disable.
-    optional bool is_user_inactive_override_from_window_manager = 33;
+    optional bool is_user_inactive_override_from_window_manager = 30;
     // The screen state to use while dozing.
-    optional .android.view.DisplayStateEnum doze_screen_state_override_from_dream_manager = 34;
+    optional .android.view.DisplayStateEnum doze_screen_state_override_from_dream_manager = 31;
     // The screen brightness to use while dozing.
-    optional float dozed_screen_brightness_override_from_dream_manager = 35;
+    optional float dozed_screen_brightness_override_from_dream_manager = 32;
     // Screen brightness settings limits.
-    optional ScreenBrightnessSettingLimitsProto screen_brightness_setting_limits = 36;
+    optional ScreenBrightnessSettingLimitsProto screen_brightness_setting_limits = 33;
     // True if double tap to wake is enabled
-    optional bool is_double_tap_wake_enabled = 37;
+    optional bool is_double_tap_wake_enabled = 34;
     // True if we are currently in VR Mode.
-    optional bool is_vr_mode_enabled = 38;
+    optional bool is_vr_mode_enabled = 35;
     // True if Sidekick is controlling the display and we shouldn't change its power mode.
-    optional bool draw_wake_lock_override_from_sidekick = 39;
+    optional bool draw_wake_lock_override_from_sidekick = 36;
 }
+
+message BatterySaverStateMachineProto {
+   // Whether battery saver is enabled.
+   optional bool enabled = 1;
+
+   // Whether system has booted.
+   optional bool boot_completed = 2;
+
+   // Whether settings have been loaded already.
+   optional bool settings_loaded = 3;
+
+   // Whether battery status has been set at least once.
+   optional bool battery_status_set = 4;
+
+   // Whether automatic battery saver has been canceled by the user.
+   optional bool battery_saver_snoozing = 5;
+
+   // Whether the device is connected to any power source.
+   optional bool is_powered = 6;
+
+   // Current battery level in %, 0-100.
+   optional int32 battery_level = 7;
+
+   // Whether battery level is low or not.
+   optional bool is_battery_level_low = 8;
+
+   // The value of Global.LOW_POWER_MODE.
+   optional bool setting_battery_saver_enabled = 9;
+
+   // The value of Global.LOW_POWER_MODE_STICKY.
+   optional bool setting_battery_saver_enabled_sticky = 10;
+
+   // The value of Global.LOW_POWER_MODE_TRIGGER_LEVEL.
+   optional int32 setting_battery_saver_trigger_threshold = 11;
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index a504ab9..6ef773a 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -266,6 +266,7 @@
                     Settings.Global.LOW_BATTERY_SOUND_TIMEOUT,
                     Settings.Global.LOW_POWER_MODE,
                     Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL_MAX,
+                    Settings.Global.LOW_POWER_MODE_STICKY,
                     Settings.Global.LTE_SERVICE_FORCED,
                     Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE,
                     Settings.Global.MAX_SOUND_TRIGGER_DETECTION_SERVICE_OPS_PER_DAY,
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index dd88cd1..2ffc4e7 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -64,6 +64,7 @@
 import android.os.WorkSource;
 import android.os.WorkSource.WorkChain;
 import android.provider.Settings;
+import android.provider.Settings.Global;
 import android.provider.Settings.SettingNotFoundException;
 import android.service.dreams.DreamManagerInternal;
 import android.service.vr.IVrManager;
@@ -97,6 +98,7 @@
 import com.android.server.lights.LightsManager;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.power.batterysaver.BatterySaverController;
+import com.android.server.power.batterysaver.BatterySaverStateMachine;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -225,6 +227,7 @@
     private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
     private final BatterySaverPolicy mBatterySaverPolicy;
     private final BatterySaverController mBatterySaverController;
+    private final BatterySaverStateMachine mBatterySaverStateMachine;
 
     private LightsManager mLightsManager;
     private BatteryManagerInternal mBatteryManagerInternal;
@@ -492,18 +495,6 @@
     // Time when we last logged a warning about calling userActivity() without permission.
     private long mLastWarningAboutUserActivityPermission = Long.MIN_VALUE;
 
-    // If true, the device is in low power mode.
-    private boolean mLowPowerModeEnabled;
-
-    // Current state of the low power mode setting.
-    private boolean mLowPowerModeSetting;
-
-    // Current state of whether the settings are allowing auto low power mode.
-    private boolean mAutoLowPowerModeConfigured;
-
-    // The user turned off low power mode below the trigger level
-    private boolean mAutoLowPowerModeSnoozing;
-
     // True if the battery level is currently considered low.
     private boolean mBatteryLevelLow;
 
@@ -667,6 +658,7 @@
         mBatterySaverPolicy = new BatterySaverPolicy(mHandler);
         mBatterySaverController = new BatterySaverController(mContext,
                 BackgroundThread.get().getLooper(), mBatterySaverPolicy);
+        mBatterySaverStateMachine = new BatterySaverStateMachine(mContext, mBatterySaverController);
 
         synchronized (mLock) {
             mWakeLockSuspendBlocker = createSuspendBlockerLocked("PowerManagerService.WakeLocks");
@@ -704,6 +696,7 @@
         mBatterySaverPolicy = batterySaverPolicy;
         mBatterySaverController = new BatterySaverController(context,
                 BackgroundThread.getHandler().getLooper(), batterySaverPolicy);
+        mBatterySaverStateMachine = new BatterySaverStateMachine(mContext, mBatterySaverController);
     }
 
     @Override
@@ -725,6 +718,8 @@
                 final long now = SystemClock.uptimeMillis();
                 mBootCompleted = true;
                 mDirty |= DIRTY_BOOT_COMPLETED;
+
+                mBatterySaverStateMachine.onBootCompleted();
                 userActivityNoUpdateLocked(
                         now, PowerManager.USER_ACTIVITY_EVENT_OTHER, 0, Process.SYSTEM_UID);
                 updatePowerStateLocked();
@@ -820,12 +815,6 @@
                 Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ),
                 false, mSettingsObserver, UserHandle.USER_ALL);
         resolver.registerContentObserver(Settings.Global.getUriFor(
-                Settings.Global.LOW_POWER_MODE),
-                false, mSettingsObserver, UserHandle.USER_ALL);
-        resolver.registerContentObserver(Settings.Global.getUriFor(
-                Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL),
-                false, mSettingsObserver, UserHandle.USER_ALL);
-        resolver.registerContentObserver(Settings.Global.getUriFor(
                 Settings.Global.THEATER_MODE_ON),
                 false, mSettingsObserver, UserHandle.USER_ALL);
         resolver.registerContentObserver(Settings.Secure.getUriFor(
@@ -953,17 +942,6 @@
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
 
-        final boolean lowPowerModeEnabled = Settings.Global.getInt(resolver,
-                Settings.Global.LOW_POWER_MODE, 0) != 0;
-        final boolean autoLowPowerModeConfigured = Settings.Global.getInt(resolver,
-                Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0) != 0;
-        if (lowPowerModeEnabled != mLowPowerModeSetting
-                || autoLowPowerModeConfigured != mAutoLowPowerModeConfigured) {
-            mLowPowerModeSetting = lowPowerModeEnabled;
-            mAutoLowPowerModeConfigured = autoLowPowerModeConfigured;
-            updateLowPowerModeLocked();
-        }
-
         mDirty |= DIRTY_SETTINGS;
     }
 
@@ -977,29 +955,6 @@
         }
     }
 
-    private void updateLowPowerModeLocked() {
-        if ((mIsPowered || !mBatteryLevelLow && !mBootCompleted) && mLowPowerModeSetting) {
-            if (DEBUG_SPEW) {
-                Slog.d(TAG, "updateLowPowerModeLocked: powered or booting with sufficient battery,"
-                        + " turning setting off");
-            }
-            // Turn setting off if powered
-            Settings.Global.putInt(mContext.getContentResolver(),
-                    Settings.Global.LOW_POWER_MODE, 0);
-            mLowPowerModeSetting = false;
-        }
-        final boolean autoLowPowerModeEnabled = !mIsPowered && mAutoLowPowerModeConfigured
-                && !mAutoLowPowerModeSnoozing && mBatteryLevelLow;
-        final boolean lowPowerModeEnabled = mLowPowerModeSetting || autoLowPowerModeEnabled;
-
-        if (mLowPowerModeEnabled != lowPowerModeEnabled) {
-            mLowPowerModeEnabled = lowPowerModeEnabled;
-
-            postAfterBootCompleted(() ->
-                    mBatterySaverController.enableBatterySaver(mLowPowerModeEnabled));
-        }
-    }
-
     private void handleSettingsChangedLocked() {
         updateSettingsLocked();
         updatePowerStateLocked();
@@ -1751,15 +1706,7 @@
                 }
             }
 
-            if (wasPowered != mIsPowered || oldLevelLow != mBatteryLevelLow) {
-                if (oldLevelLow != mBatteryLevelLow && !mBatteryLevelLow) {
-                    if (DEBUG_SPEW) {
-                        Slog.d(TAG, "updateIsPoweredLocked: resetting low power snooze");
-                    }
-                    mAutoLowPowerModeSnoozing = false;
-                }
-                updateLowPowerModeLocked();
-            }
+            mBatterySaverStateMachine.setBatteryStatus(mIsPowered, mBatteryLevel, mBatteryLevelLow);
         }
     }
 
@@ -2733,36 +2680,20 @@
     }
 
     private boolean isLowPowerModeInternal() {
-        synchronized (mLock) {
-            return mLowPowerModeEnabled;
-        }
+        return mBatterySaverController.isEnabled();
     }
 
-    private boolean setLowPowerModeInternal(boolean mode) {
+    private boolean setLowPowerModeInternal(boolean enabled) {
         synchronized (mLock) {
-            if (DEBUG) Slog.d(TAG, "setLowPowerModeInternal " + mode + " mIsPowered=" + mIsPowered);
+            if (DEBUG) {
+                Slog.d(TAG, "setLowPowerModeInternal " + enabled + " mIsPowered=" + mIsPowered);
+            }
             if (mIsPowered) {
                 return false;
             }
-            Settings.Global.putInt(mContext.getContentResolver(),
-                    Settings.Global.LOW_POWER_MODE, mode ? 1 : 0);
-            mLowPowerModeSetting = mode;
 
-            if (mAutoLowPowerModeConfigured && mBatteryLevelLow) {
-                if (mode && mAutoLowPowerModeSnoozing) {
-                    if (DEBUG_SPEW) {
-                        Slog.d(TAG, "setLowPowerModeInternal: clearing low power mode snooze");
-                    }
-                    mAutoLowPowerModeSnoozing = false;
-                } else if (!mode && !mAutoLowPowerModeSnoozing) {
-                    if (DEBUG_SPEW) {
-                        Slog.d(TAG, "setLowPowerModeInternal: snoozing low power mode");
-                    }
-                    mAutoLowPowerModeSnoozing = true;
-                }
-            }
+            mBatterySaverStateMachine.setBatterySaverEnabledManually(enabled);
 
-            updateLowPowerModeLocked();
             return true;
         }
     }
@@ -2848,7 +2779,8 @@
     @VisibleForTesting
     void updatePowerRequestFromBatterySaverPolicy(DisplayPowerRequest displayPowerRequest) {
         PowerSaveState state = mBatterySaverPolicy.
-                getBatterySaverPolicy(ServiceType.SCREEN_BRIGHTNESS, mLowPowerModeEnabled);
+                getBatterySaverPolicy(ServiceType.SCREEN_BRIGHTNESS,
+                        mBatterySaverController.isEnabled());
         displayPowerRequest.lowPowerMode = state.batterySaverEnabled;
         displayPowerRequest.screenLowPowerBrightnessFactor = state.brightnessFactor;
     }
@@ -3325,7 +3257,6 @@
             pw.println("  mRequestWaitForNegativeProximity=" + mRequestWaitForNegativeProximity);
             pw.println("  mSandmanScheduled=" + mSandmanScheduled);
             pw.println("  mSandmanSummoned=" + mSandmanSummoned);
-            pw.println("  mLowPowerModeEnabled=" + mLowPowerModeEnabled);
             pw.println("  mBatteryLevelLow=" + mBatteryLevelLow);
             pw.println("  mLightDeviceIdleMode=" + mLightDeviceIdleMode);
             pw.println("  mDeviceIdleMode=" + mDeviceIdleMode);
@@ -3378,9 +3309,6 @@
             pw.println("  mDreamsActivateOnSleepSetting=" + mDreamsActivateOnSleepSetting);
             pw.println("  mDreamsActivateOnDockSetting=" + mDreamsActivateOnDockSetting);
             pw.println("  mDozeAfterScreenOff=" + mDozeAfterScreenOff);
-            pw.println("  mLowPowerModeSetting=" + mLowPowerModeSetting);
-            pw.println("  mAutoLowPowerModeConfigured=" + mAutoLowPowerModeConfigured);
-            pw.println("  mAutoLowPowerModeSnoozing=" + mAutoLowPowerModeSnoozing);
             pw.println("  mMinimumScreenOffTimeoutConfig=" + mMinimumScreenOffTimeoutConfig);
             pw.println("  mMaximumScreenDimDurationConfig=" + mMaximumScreenDimDurationConfig);
             pw.println("  mMaximumScreenDimRatioConfig=" + mMaximumScreenDimRatioConfig);
@@ -3456,6 +3384,7 @@
             pw.println("Display Power: " + mDisplayPowerCallbacks);
 
             mBatterySaverPolicy.dump(pw);
+            mBatterySaverStateMachine.dump(pw);
 
             pw.println();
             final int numProfiles = mProfilePowerState.size();
@@ -3557,7 +3486,6 @@
                     mRequestWaitForNegativeProximity);
             proto.write(PowerManagerServiceDumpProto.IS_SANDMAN_SCHEDULED, mSandmanScheduled);
             proto.write(PowerManagerServiceDumpProto.IS_SANDMAN_SUMMONED, mSandmanSummoned);
-            proto.write(PowerManagerServiceDumpProto.IS_LOW_POWER_MODE_ENABLED, mLowPowerModeEnabled);
             proto.write(PowerManagerServiceDumpProto.IS_BATTERY_LEVEL_LOW, mBatteryLevelLow);
             proto.write(PowerManagerServiceDumpProto.IS_LIGHT_DEVICE_IDLE_MODE, mLightDeviceIdleMode);
             proto.write(PowerManagerServiceDumpProto.IS_DEVICE_IDLE_MODE, mDeviceIdleMode);
@@ -3663,15 +3591,6 @@
                     PowerServiceSettingsAndConfigurationDumpProto.IS_DOZE_AFTER_SCREEN_OFF_CONFIG,
                     mDozeAfterScreenOff);
             proto.write(
-                    PowerServiceSettingsAndConfigurationDumpProto.IS_LOW_POWER_MODE_SETTING,
-                    mLowPowerModeSetting);
-            proto.write(
-                    PowerServiceSettingsAndConfigurationDumpProto.IS_AUTO_LOW_POWER_MODE_CONFIGURED,
-                    mAutoLowPowerModeConfigured);
-            proto.write(
-                    PowerServiceSettingsAndConfigurationDumpProto.IS_AUTO_LOW_POWER_MODE_SNOOZING,
-                    mAutoLowPowerModeSnoozing);
-            proto.write(
                     PowerServiceSettingsAndConfigurationDumpProto
                             .MINIMUM_SCREEN_OFF_TIMEOUT_CONFIG_MS,
                     mMinimumScreenOffTimeoutConfig);
@@ -3792,6 +3711,9 @@
                 proto.end(uIDToken);
             }
 
+            mBatterySaverStateMachine.dumpProto(proto,
+                    PowerManagerServiceDumpProto.BATTERY_SAVER_STATE_MACHINE);
+
             mHandler.getLooper().writeToProto(proto, PowerManagerServiceDumpProto.LOOPER);
 
             for (WakeLock wl : mWakeLocks) {
@@ -4432,12 +4354,12 @@
         }
 
         @Override // Binder call
-        public boolean setPowerSaveMode(boolean mode) {
+        public boolean setPowerSaveMode(boolean enabled) {
             mContext.enforceCallingOrSelfPermission(
                     android.Manifest.permission.DEVICE_POWER, null);
             final long ident = Binder.clearCallingIdentity();
             try {
-                return setLowPowerModeInternal(mode);
+                return setLowPowerModeInternal(enabled);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -4752,7 +4674,8 @@
         @Override
         public PowerSaveState getLowPowerState(@ServiceType int serviceType) {
             synchronized (mLock) {
-                return mBatterySaverPolicy.getBatterySaverPolicy(serviceType, mLowPowerModeEnabled);
+                return mBatterySaverPolicy.getBatterySaverPolicy(serviceType,
+                        mBatterySaverController.isEnabled());
             }
         }
 
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java
new file mode 100644
index 0000000..5b3182e
--- /dev/null
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java
@@ -0,0 +1,413 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.power.BatterySaverPolicy;
+import com.android.server.power.BatterySaverStateMachineProto;
+
+import java.io.PrintWriter;
+
+/**
+ * Decides when to enable / disable battery saver.
+ *
+ * (n.b. This isn't really implemented as a "state machine" though.)
+ *
+ * Test:
+  atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java
+ */
+public class BatterySaverStateMachine {
+    private static final String TAG = "BatterySaverStateMachine";
+    private final Object mLock = new Object();
+
+    private static final boolean DEBUG = BatterySaverPolicy.DEBUG;
+
+    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;
+
+    /** 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 Global.LOW_POWER_MODE. */
+    @GuardedBy("mLock")
+    private boolean mSettingBatterySaverEnabled;
+
+    /** Previously known value of Global.LOW_POWER_MODE_STICKY. */
+    @GuardedBy("mLock")
+    private boolean mSettingBatterySaverEnabledSticky;
+
+    /**
+     * Previously known value of Global.LOW_POWER_MODE_TRIGGER_LEVEL.
+     * (Currently only used in dumpsys.)
+     */
+    @GuardedBy("mLock")
+    private int mSettingBatterySaverTriggerThreshold;
+
+    /**
+     * Whether BS has been manually disabled while the battery level is low, in which case we
+     * shouldn't auto re-enable it until the battery level is not low.
+     */
+    @GuardedBy("mLock")
+    private boolean mBatterySaverSnoozing;
+
+    private final ContentObserver mSettingsObserver = new ContentObserver(null) {
+        @Override
+        public void onChange(boolean selfChange) {
+            synchronized (mLock) {
+                refreshSettingsLocked();
+            }
+        }
+    };
+
+    public BatterySaverStateMachine(
+            Context context, BatterySaverController batterySaverController) {
+        mContext = context;
+        mBatterySaverController = batterySaverController;
+    }
+
+    private boolean isBatterySaverEnabled() {
+        return mBatterySaverController.isEnabled();
+    }
+
+    private boolean isAutoBatterySaverConfigured() {
+        return mSettingBatterySaverTriggerThreshold > 0;
+    }
+
+    /**
+     * {@link com.android.server.power.PowerManagerService} calls it when the system is booted.
+     */
+    public void onBootCompleted() {
+        if (DEBUG) {
+            Slog.d(TAG, "onBootCompleted");
+        }
+        synchronized (mLock) {
+
+            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);
+
+            mBootCompleted = true;
+
+            refreshSettingsLocked();
+
+            doAutoBatterySaverLocked();
+        }
+    }
+
+    void refreshSettingsLocked() {
+        final ContentResolver cr = mContext.getContentResolver();
+
+        final boolean lowPowerModeEnabled = getGlobalSetting(
+                Settings.Global.LOW_POWER_MODE, 0) != 0;
+        final boolean lowPowerModeEnabledSticky = getGlobalSetting(
+                Settings.Global.LOW_POWER_MODE_STICKY, 0) != 0;
+        final int lowPowerModeTriggerLevel = getGlobalSetting(
+                Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+
+        setSettingsLocked(lowPowerModeEnabled, lowPowerModeEnabledSticky,
+                lowPowerModeTriggerLevel);
+    }
+
+    /**
+     * {@link com.android.server.power.PowerManagerService} calls it when relevant global settings
+     * have changed.
+     *
+     * Note this will be called before {@link #onBootCompleted} too.
+     */
+    @VisibleForTesting
+    void setSettingsLocked(boolean batterySaverEnabled, boolean batterySaverEnabledSticky,
+            int batterySaverTriggerThreshold) {
+        if (DEBUG) {
+            Slog.d(TAG, "setSettings: enabled=" + batterySaverEnabled
+                    + " sticky=" + batterySaverEnabledSticky
+                    + " threshold=" + batterySaverTriggerThreshold);
+        }
+
+        mSettingsLoaded = true;
+
+        final boolean enabledChanged = mSettingBatterySaverEnabled != batterySaverEnabled;
+        final boolean stickyChanged =
+                mSettingBatterySaverEnabledSticky != batterySaverEnabledSticky;
+        final boolean thresholdChanged
+                = mSettingBatterySaverTriggerThreshold != batterySaverTriggerThreshold;
+
+        if (!(enabledChanged || stickyChanged || thresholdChanged)) {
+            return;
+        }
+
+        mSettingBatterySaverEnabled = batterySaverEnabled;
+        mSettingBatterySaverEnabledSticky = batterySaverEnabledSticky;
+        mSettingBatterySaverTriggerThreshold = batterySaverTriggerThreshold;
+
+        if (enabledChanged) {
+            final String reason = batterySaverEnabled
+                    ? "Global.low_power changed to 1" : "Global.low_power changed to 0";
+            enableBatterySaverLocked(/*enable=*/ batterySaverEnabled, /*manual=*/ true,
+                    reason);
+        }
+    }
+
+    /**
+     * {@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();
+        }
+    }
+
+    /**
+     * Decide whether to auto-start / stop battery saver.
+     */
+    private void doAutoBatterySaverLocked() {
+        if (DEBUG) {
+            Slog.d(TAG, "doAutoBatterySaverLocked: mBootCompleted=" + mBootCompleted
+                    + " mSettingsLoaded=" + mSettingsLoaded
+                    + " mBatteryStatusSet=" + mBatteryStatusSet
+                    + " mIsBatteryLevelLow=" + mIsBatteryLevelLow
+                    + " mBatterySaverSnoozing=" + mBatterySaverSnoozing
+                    + " mIsPowered=" + mIsPowered
+                    + " mSettingBatterySaverEnabledSticky=" + mSettingBatterySaverEnabledSticky);
+        }
+        if (!(mBootCompleted && mSettingsLoaded && mBatteryStatusSet)) {
+            return; // Not fully initialized yet.
+        }
+        if (!mIsBatteryLevelLow) {
+            updateSnoozingLocked(false, "Battery not low");
+        }
+        if (mIsPowered) {
+            updateSnoozingLocked(false, "Plugged in");
+            enableBatterySaverLocked(/*enable=*/ false, /*manual=*/ false, "Plugged in");
+
+        } else if (mSettingBatterySaverEnabledSticky) {
+            // Re-enable BS.
+            enableBatterySaverLocked(/*enable=*/ true, /*manual=*/ true, "Sticky restore");
+
+        } else if (mIsBatteryLevelLow) {
+            if (!mBatterySaverSnoozing && isAutoBatterySaverConfigured()) {
+                enableBatterySaverLocked(/*enable=*/ true, /*manual=*/ false, "Auto ON");
+            }
+        } else { // Battery not low
+            enableBatterySaverLocked(/*enable=*/ false, /*manual=*/ false, "Auto OFF");
+        }
+    }
+
+    /**
+     * {@link com.android.server.power.PowerManagerService} calls it when
+     * {@link android.os.PowerManager#setPowerSaveMode} 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) {
+            enableBatterySaverLocked(/*enable=*/ enabled, /*manual=*/ true,
+                    (enabled ? "Manual ON" : "Manual OFF"));
+        }
+    }
+
+    /**
+     * Actually enable / disable battery saver. Write the new state to the global settings
+     * and propagate it to {@link #mBatterySaverController}.
+     */
+    private void enableBatterySaverLocked(boolean enable, boolean manual, String reason) {
+        if (DEBUG) {
+            Slog.d(TAG, "enableBatterySaver: enable=" + enable + " manual=" + manual
+                    + " reason=" + reason);
+        }
+        final boolean wasEnabled = mBatterySaverController.isEnabled();
+
+        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;
+        }
+
+        if (manual) {
+            if (enable) {
+                updateSnoozingLocked(false, "Manual snooze OFF");
+            } else {
+                // 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.
+                if (isBatterySaverEnabled() && mIsBatteryLevelLow) {
+                    updateSnoozingLocked(true, "Manual snooze");
+                }
+            }
+        }
+
+        mSettingBatterySaverEnabled = enable;
+        putGlobalSetting(Global.LOW_POWER_MODE, enable ? 1 : 0);
+
+        if (manual) {
+            mSettingBatterySaverEnabledSticky = enable;
+            putGlobalSetting(Global.LOW_POWER_MODE_STICKY, enable ? 1 : 0);
+        }
+        mBatterySaverController.enableBatterySaver(enable);
+
+        if (DEBUG) {
+            Slog.d(TAG, "Battery saver: Enabled=" + enable
+                    + " manual=" + manual
+                    + " reason=" + reason);
+        }
+    }
+
+    private void updateSnoozingLocked(boolean snoozing, String reason) {
+        if (mBatterySaverSnoozing == snoozing) {
+            return;
+        }
+        if (DEBUG) Slog.d(TAG, "Snooze: " + (snoozing ? "start" : "stop")  + " reason=" + reason);
+        mBatterySaverSnoozing = snoozing;
+    }
+
+    @VisibleForTesting
+    protected void putGlobalSetting(String key, int value) {
+        Global.putInt(mContext.getContentResolver(), key, value);
+    }
+
+    @VisibleForTesting
+    protected int getGlobalSetting(String key, int defValue) {
+        return 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("  mBootCompleted=");
+            pw.println(mBootCompleted);
+            pw.print("  mSettingsLoaded=");
+            pw.println(mSettingsLoaded);
+            pw.print("  mBatteryStatusSet=");
+            pw.println(mBatteryStatusSet);
+
+            pw.print("  mBatterySaverSnoozing=");
+            pw.println(mBatterySaverSnoozing);
+
+            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("  mSettingBatterySaverTriggerThreshold=");
+            pw.println(mSettingBatterySaverTriggerThreshold);
+        }
+    }
+
+    public void dumpProto(ProtoOutputStream proto, long tag) {
+        synchronized (mLock) {
+            final long token = proto.start(tag);
+
+            proto.write(BatterySaverStateMachineProto.ENABLED,
+                    mBatterySaverController.isEnabled());
+
+            proto.write(BatterySaverStateMachineProto.BOOT_COMPLETED, mBootCompleted);
+            proto.write(BatterySaverStateMachineProto.SETTINGS_LOADED, mSettingsLoaded);
+            proto.write(BatterySaverStateMachineProto.BATTERY_STATUS_SET, mBatteryStatusSet);
+
+            proto.write(BatterySaverStateMachineProto.BATTERY_SAVER_SNOOZING,
+                    mBatterySaverSnoozing);
+
+            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.end(token);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java
new file mode 100644
index 0000000..ab640d6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java
@@ -0,0 +1,532 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.provider.Settings.Global;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.mock.MockContext;
+
+import com.google.common.base.Objects;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+
+/**
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverStateMachineTest.java
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BatterySaverStateMachineTest {
+
+    private MyMockContext mMockContext;
+    private ContentResolver mMockContextResolver;
+    private BatterySaverController mMockBatterySaverController;
+    private Device mDevice;
+    private TestableBatterySaverStateMachine mTarget;
+
+    private class MyMockContext extends MockContext {
+        @Override
+        public ContentResolver getContentResolver() {
+            return mMockContextResolver;
+        }
+    }
+
+    private DevicePersistedState mPersistedState;
+
+    private class DevicePersistedState {
+        // Current battery level.
+        public int batteryLevel = 100;
+
+        // Whether battery level is currently low or not.
+        public boolean batteryLow = false;
+
+        // Whether the device is plugged in or not.
+        public boolean powered = false;
+
+        // Global settings.
+        public final HashMap<String, Integer> global = new HashMap<>();
+    }
+
+    /**
+     * This class simulates a device's volatile status that will be reset by {@link #initDevice()}.
+     */
+    private class Device {
+        public boolean batterySaverEnabled = false;
+
+        public int getLowPowerModeTriggerLevel() {
+            return mPersistedState.global.getOrDefault(Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+        }
+
+        public void setBatteryLevel(int level) {
+            mPersistedState.batteryLevel = level;
+            if (mPersistedState.batteryLevel <= Math.max(15, getLowPowerModeTriggerLevel())) {
+                mPersistedState.batteryLow = true;
+            } else if (mPersistedState.batteryLow
+                    && (mPersistedState.batteryLevel >= (getLowPowerModeTriggerLevel() + 5))) {
+                mPersistedState.batteryLow = false;
+            }
+            pushBatteryStatus();
+        }
+
+        public void setPowered(boolean newPowered) {
+            mPersistedState.powered = newPowered;
+            pushBatteryStatus();
+        }
+
+        public void pushBatteryStatus() {
+            mTarget.setBatteryStatus(mPersistedState.powered, mPersistedState.batteryLevel,
+                    mPersistedState.batteryLow);
+        }
+
+        public void pushGlobalSettings() {
+            mTarget.setSettingsLocked(
+                    mPersistedState.global.getOrDefault(Global.LOW_POWER_MODE, 0) != 0,
+                    mPersistedState.global.getOrDefault(Global.LOW_POWER_MODE_STICKY, 0) != 0,
+                    mDevice.getLowPowerModeTriggerLevel());
+        }
+
+        public void putGlobalSetting(String key, int value) {
+            mPersistedState.global.put(key, value);
+            pushGlobalSettings();
+        }
+
+        public int getGlobalSetting(String key, int defValue) {
+            return mPersistedState.global.getOrDefault(key, defValue);
+        }
+    }
+
+    /**
+     * Test target class.
+     */
+    private class TestableBatterySaverStateMachine extends BatterySaverStateMachine {
+        public TestableBatterySaverStateMachine() {
+            super(mMockContext, mMockBatterySaverController);
+        }
+
+        @Override
+        protected void putGlobalSetting(String key, int value) {
+            if (Objects.equal(mPersistedState.global.get(key), value)) {
+                return;
+            }
+            mDevice.putGlobalSetting(key, value);
+        }
+
+        @Override
+        protected int getGlobalSetting(String key, int defValue) {
+            return mDevice.getGlobalSetting(key, defValue);
+        }
+    }
+
+    @Before
+    public void setUp() {
+        mMockContext = new MyMockContext();
+        mMockContextResolver = mock(ContentResolver.class);
+        mMockBatterySaverController = mock(BatterySaverController.class);
+
+        doAnswer((inv) -> mDevice.batterySaverEnabled = inv.getArgument(0))
+                .when(mMockBatterySaverController).enableBatterySaver(anyBoolean());
+        when(mMockBatterySaverController.isEnabled())
+                .thenAnswer((inv) -> mDevice.batterySaverEnabled);
+
+        mPersistedState = new DevicePersistedState();
+        initDevice();
+    }
+
+    private void initDevice() {
+        mDevice = new Device();
+
+        mTarget = new TestableBatterySaverStateMachine();
+
+        mDevice.pushBatteryStatus();
+        mDevice.pushGlobalSettings();
+        mTarget.onBootCompleted();
+    }
+
+    @Test
+    public void testNoAutoBatterySaver() {
+        assertEquals(0, mDevice.getLowPowerModeTriggerLevel());
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(100, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(90);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(50);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(50, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(16);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(16, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // When LOW_POWER_MODE_TRIGGER_LEVEL is 0, 15% will still trigger low-battery, but
+        // BS wont be enabled.
+        mDevice.setBatteryLevel(15);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(15, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(10);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(10, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        // Manually enable BS.
+        mTarget.setBatterySaverEnabledManually(true);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(10, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(50);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(50, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Start charging. It'll disable BS.
+        mDevice.setPowered(true);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(50, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(60);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(60, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Unplug.
+        mDevice.setPowered(false);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(60, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(10);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(10, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(80);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(80, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Reboot the device.
+        initDevice();
+
+        assertEquals(true, mDevice.batterySaverEnabled); // Sticky.
+        assertEquals(80, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(30);
+        initDevice();
+
+        assertEquals(true, mDevice.batterySaverEnabled); // Still sticky.
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mTarget.setBatterySaverEnabledManually(false);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        initDevice(); // reboot.
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+    }
+
+    @Test
+    public void testAutoBatterySaver() {
+        mDevice.putGlobalSetting(Global.LOW_POWER_MODE_TRIGGER_LEVEL, 50);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(100, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(90);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(51);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(51, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Hit the threshold. BS should be enabled.
+        mDevice.setBatteryLevel(50);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(50, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        // Battery goes up, but until it hits 55%, we still keep BS on.
+        mDevice.setBatteryLevel(54);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(54, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        // 50% + 5%, now BS will be off.
+        mDevice.setBatteryLevel(55);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(55, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(40);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(40, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setPowered(true);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(40, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setPowered(false);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(40, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mTarget.setBatterySaverEnabledManually(false); // Manually disable -> snooze.
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(40, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(30);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        // Plug in and out, snooze will reset.
+        mDevice.setPowered(true);
+        mDevice.setPowered(false);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setPowered(true);
+        mDevice.setBatteryLevel(60);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(60, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setPowered(false);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(60, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(50);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(50, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(70);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(70, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Bump ump the threshold.
+        mDevice.putGlobalSetting(Global.LOW_POWER_MODE_TRIGGER_LEVEL, 70);
+        mDevice.setBatteryLevel(mPersistedState.batteryLevel);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(70, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        // Then down.
+        mDevice.putGlobalSetting(Global.LOW_POWER_MODE_TRIGGER_LEVEL, 60);
+        mDevice.setBatteryLevel(mPersistedState.batteryLevel);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(70, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Reboot in low state -> automatically enable BS.
+        mDevice.setPowered(false);
+        mDevice.setBatteryLevel(30);
+        mTarget.setBatterySaverEnabledManually(false);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        initDevice();
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+    }
+
+    @Test
+    public void testAutoBatterySaver_withSticky() {
+        mDevice.putGlobalSetting(Global.LOW_POWER_MODE_TRIGGER_LEVEL, 50);
+
+        mTarget.setBatterySaverEnabledManually(true);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(100, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(30);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(80);
+
+        assertEquals(true, mDevice.batterySaverEnabled); // Still enabled.
+        assertEquals(80, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setPowered(true);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(80, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(30);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setPowered(false);
+
+        assertEquals(true, mDevice.batterySaverEnabled); // Restores BS.
+        assertEquals(30, mPersistedState.batteryLevel);
+        assertEquals(true, mPersistedState.batteryLow);
+
+        mDevice.setPowered(true);
+        mDevice.setBatteryLevel(90);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        initDevice();
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setPowered(false);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mTarget.setBatterySaverEnabledManually(false);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        initDevice();
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+    }
+
+    @Test
+    public void testNoAutoBatterySaver_fromAdb() {
+
+        assertEquals(0, mDevice.getLowPowerModeTriggerLevel());
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(100, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        mDevice.setBatteryLevel(90);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Enable
+        mDevice.putGlobalSetting(Global.LOW_POWER_MODE, 1);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Disable
+        mDevice.putGlobalSetting(Global.LOW_POWER_MODE, 0);
+
+        assertEquals(false, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Enable again
+        mDevice.putGlobalSetting(Global.LOW_POWER_MODE, 1);
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+
+        // Reboot -- setting BS from adb is also sticky.
+        initDevice();
+
+        assertEquals(true, mDevice.batterySaverEnabled);
+        assertEquals(90, mPersistedState.batteryLevel);
+        assertEquals(false, mPersistedState.batteryLow);
+    }
+}