content: /*
   * 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) {
              triggerDynamicModeNotification();
          } else if (!enable) {
              hideDynamicModeNotification();
          }
  
          if (DEBUG) {
              Slog.d(TAG, "Battery saver: Enabled=" + enable
                      + " manual=" + manual
                      + " reason=" + strReason + "(" + intReason + ")");
          }
      }
  
      @VisibleForTesting
      void triggerDynamicModeNotification() {
          // The current lock is the PowerManager lock, which sits very low in the service lock
          // hierarchy. We shouldn't call out to NotificationManager with the PowerManager lock.
          runOnBgThread(() -> {
              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() {
          // The current lock is the PowerManager lock, which sits very low in the service lock
          // hierarchy. We shouldn't call out to NotificationManager with the PowerManager lock.
          runOnBgThread(() -> {
              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) {
          // The current lock is the PowerManager lock, which sits very low in the service lock
          // hierarchy. We shouldn't call out to NotificationManager with the PowerManager lock.
          runOnBgThread(() -> {
              NotificationManager manager = mContext.getSystemService(NotificationManager.class);
              manager.cancelAsUser(TAG, notificationId, UserHandle.ALL);
          });
      }
  
      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);
          }
      }
  }
  