content: /*
   * Copyright (C) 2017 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.Manifest;
  import android.app.ActivityManagerInternal;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.hardware.power.V1_0.PowerHint;
  import android.os.BatteryManager;
  import android.os.BatterySaverPolicyConfig;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.PowerManager;
  import android.os.PowerManagerInternal;
  import android.os.PowerManagerInternal.LowPowerModeListener;
  import android.os.PowerSaveState;
  import android.os.UserHandle;
  import android.util.ArrayMap;
  import android.util.Slog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.ArrayUtils;
  import com.android.internal.util.Preconditions;
  import com.android.server.EventLogTags;
  import com.android.server.LocalServices;
  import com.android.server.power.PowerManagerService;
  import com.android.server.power.batterysaver.BatterySaverPolicy.BatterySaverPolicyListener;
  import com.android.server.power.batterysaver.BatterySaverPolicy.Policy;
  import com.android.server.power.batterysaver.BatterySavingStats.BatterySaverState;
  import com.android.server.power.batterysaver.BatterySavingStats.DozeState;
  import com.android.server.power.batterysaver.BatterySavingStats.InteractiveState;
  
  import java.util.ArrayList;
  
  /**
   * Responsible for battery saver mode transition logic.
   *
   * 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.)
   */
  public class BatterySaverController implements BatterySaverPolicyListener {
      static final String TAG = "BatterySaverController";
  
      static final boolean DEBUG = BatterySaverPolicy.DEBUG;
  
      private final Object mLock;
      private final Context mContext;
      private final MyHandler mHandler;
      private final FileUpdater mFileUpdater;
  
      private PowerManager mPowerManager;
  
      private final BatterySaverPolicy mBatterySaverPolicy;
  
      private final BatterySavingStats mBatterySavingStats;
  
      @GuardedBy("mLock")
      private final ArrayList<LowPowerModeListener> mListeners = new ArrayList<>();
  
      @GuardedBy("mLock")
      private boolean mFullEnabled;
  
      @GuardedBy("mLock")
      private boolean mAdaptiveEnabled;
  
      @GuardedBy("mLock")
      private boolean mIsPluggedIn;
  
      /**
       * Whether full was previously enabled or not; only for the event logging. Only use it from
       * {@link #handleBatterySaverStateChanged}.
       */
      private boolean mFullPreviouslyEnabled;
  
      /**
       * Whether adaptive was previously enabled or not; only for the event logging. Only use it from
       * {@link #handleBatterySaverStateChanged}.
       */
      private boolean mAdaptivePreviouslyEnabled;
  
      @GuardedBy("mLock")
      private boolean mIsInteractive;
  
      /**
       * Read-only list of plugins. No need for synchronization.
       */
      private final Plugin[] mPlugins;
  
      public static final int REASON_PERCENTAGE_AUTOMATIC_ON = 0;
      public static final int REASON_PERCENTAGE_AUTOMATIC_OFF = 1;
      public static final int REASON_MANUAL_ON = 2;
      public static final int REASON_MANUAL_OFF = 3;
      public static final int REASON_STICKY_RESTORE = 4;
      public static final int REASON_INTERACTIVE_CHANGED = 5;
      public static final int REASON_POLICY_CHANGED = 6;
      public static final int REASON_PLUGGED_IN = 7;
      public static final int REASON_SETTING_CHANGED = 8;
      public static final int REASON_DYNAMIC_POWER_SAVINGS_AUTOMATIC_ON = 9;
      public static final int REASON_DYNAMIC_POWER_SAVINGS_AUTOMATIC_OFF = 10;
      public static final int REASON_ADAPTIVE_DYNAMIC_POWER_SAVINGS_CHANGED = 11;
      public static final int REASON_TIMEOUT = 12;
  
      static String reasonToString(int reason) {
          switch (reason) {
              case BatterySaverController.REASON_PERCENTAGE_AUTOMATIC_ON:
                  return "Percentage Auto ON";
              case BatterySaverController.REASON_PERCENTAGE_AUTOMATIC_OFF:
                  return "Percentage Auto OFF";
              case BatterySaverController.REASON_MANUAL_ON:
                  return "Manual ON";
              case BatterySaverController.REASON_MANUAL_OFF:
                  return "Manual OFF";
              case BatterySaverController.REASON_STICKY_RESTORE:
                  return "Sticky restore";
              case BatterySaverController.REASON_INTERACTIVE_CHANGED:
                  return "Interactivity changed";
              case BatterySaverController.REASON_POLICY_CHANGED:
                  return "Policy changed";
              case BatterySaverController.REASON_PLUGGED_IN:
                  return "Plugged in";
              case BatterySaverController.REASON_SETTING_CHANGED:
                  return "Setting changed";
              case BatterySaverController.REASON_DYNAMIC_POWER_SAVINGS_AUTOMATIC_ON:
                  return "Dynamic Warning Auto ON";
              case BatterySaverController.REASON_DYNAMIC_POWER_SAVINGS_AUTOMATIC_OFF:
                  return "Dynamic Warning Auto OFF";
              case BatterySaverController.REASON_ADAPTIVE_DYNAMIC_POWER_SAVINGS_CHANGED:
                  return "Adaptive Power Savings changed";
              case BatterySaverController.REASON_TIMEOUT:
                  return "timeout";
              default:
                  return "Unknown reason: " + reason;
          }
      }
  
      /**
       * Plugin interface. All methods are guaranteed to be called on the same (handler) thread.
       */
      public interface Plugin {
          void onSystemReady(BatterySaverController caller);
  
          void onBatterySaverChanged(BatterySaverController caller);
      }
  
      private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (DEBUG) {
                  Slog.d(TAG, "onReceive: " + intent);
              }
              switch (intent.getAction()) {
                  case Intent.ACTION_SCREEN_ON:
                  case Intent.ACTION_SCREEN_OFF:
                      if (!isPolicyEnabled()) {
                          updateBatterySavingStats();
                          return; // No need to send it if not enabled.
                      }
                      // Don't send the broadcast, because we never did so in this case.
                      mHandler.postStateChanged(/*sendBroadcast=*/ false,
                              REASON_INTERACTIVE_CHANGED);
                      break;
                  case Intent.ACTION_BATTERY_CHANGED:
                      synchronized (mLock) {
                          mIsPluggedIn = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0);
                      }
                      // Fall-through.
                  case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED:
                  case PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED:
                      updateBatterySavingStats();
                      break;
              }
          }
      };
  
      /**
       * Constructor.
       */
      public BatterySaverController(Object lock, Context context, Looper looper,
              BatterySaverPolicy policy, BatterySavingStats batterySavingStats) {
          mLock = lock;
          mContext = context;
          mHandler = new MyHandler(looper);
          mBatterySaverPolicy = policy;
          mBatterySaverPolicy.addListener(this);
          mFileUpdater = new FileUpdater(context);
          mBatterySavingStats = batterySavingStats;
  
          // Initialize plugins.
          mPlugins = new Plugin[] {
                  new BatterySaverLocationPlugin(mContext)
          };
      }
  
      /**
       * Add a listener.
       */
      public void addListener(LowPowerModeListener listener) {
          synchronized (mLock) {
              mListeners.add(listener);
          }
      }
  
      /**
       * Called by {@link PowerManagerService} on system ready, *with no lock held*.
       */
      public void systemReady() {
          final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
          filter.addAction(Intent.ACTION_SCREEN_OFF);
          filter.addAction(Intent.ACTION_BATTERY_CHANGED);
          filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
          filter.addAction(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);
          mContext.registerReceiver(mReceiver, filter);
  
          mFileUpdater.systemReady(LocalServices.getService(ActivityManagerInternal.class)
                  .isRuntimeRestarted());
          mHandler.postSystemReady();
      }
  
      private PowerManager getPowerManager() {
          if (mPowerManager == null) {
              mPowerManager =
                      Preconditions.checkNotNull(mContext.getSystemService(PowerManager.class));
          }
          return mPowerManager;
      }
  
      @Override
      public void onBatterySaverPolicyChanged(BatterySaverPolicy policy) {
          if (!isPolicyEnabled()) {
              return; // No need to send it if not enabled.
          }
          mHandler.postStateChanged(/*sendBroadcast=*/ true, REASON_POLICY_CHANGED);
      }
  
      private class MyHandler extends Handler {
          private static final int MSG_STATE_CHANGED = 1;
  
          private static final int ARG_DONT_SEND_BROADCAST = 0;
          private static final int ARG_SEND_BROADCAST = 1;
  
          private static final int MSG_SYSTEM_READY = 2;
  
          public MyHandler(Looper looper) {
              super(looper);
          }
  
          void postStateChanged(boolean sendBroadcast, int reason) {
              obtainMessage(MSG_STATE_CHANGED, sendBroadcast ?
                      ARG_SEND_BROADCAST : ARG_DONT_SEND_BROADCAST, reason).sendToTarget();
          }
  
          public void postSystemReady() {
              obtainMessage(MSG_SYSTEM_READY, 0, 0).sendToTarget();
          }
  
          @Override
          public void dispatchMessage(Message msg) {
              switch (msg.what) {
                  case MSG_STATE_CHANGED:
                      handleBatterySaverStateChanged(
                              msg.arg1 == ARG_SEND_BROADCAST,
                              msg.arg2);
                      break;
  
                  case MSG_SYSTEM_READY:
                      for (Plugin p : mPlugins) {
                          p.onSystemReady(BatterySaverController.this);
                      }
                      break;
              }
          }
      }
  
      /** Enable or disable full battery saver. */
      @VisibleForTesting
      public void enableBatterySaver(boolean enable, int reason) {
          synchronized (mLock) {
              if (mFullEnabled == enable) {
                  return;
              }
              mFullEnabled = enable;
  
              if (updatePolicyLevelLocked()) {
                  mHandler.postStateChanged(/*sendBroadcast=*/ true, reason);
              }
          }
      }
  
      private boolean updatePolicyLevelLocked() {
          if (mFullEnabled) {
              return mBatterySaverPolicy.setPolicyLevel(BatterySaverPolicy.POLICY_LEVEL_FULL);
          } else if (mAdaptiveEnabled) {
              return mBatterySaverPolicy.setPolicyLevel(BatterySaverPolicy.POLICY_LEVEL_ADAPTIVE);
          } else {
              return mBatterySaverPolicy.setPolicyLevel(BatterySaverPolicy.POLICY_LEVEL_OFF);
          }
      }
  
      /**
       * @return whether battery saver is enabled or not. This takes into
       * account whether a policy says to advertise isEnabled so this can be propagated externally.
       */
      public boolean isEnabled() {
          synchronized (mLock) {
              return mFullEnabled
                      || (mAdaptiveEnabled && mBatterySaverPolicy.shouldAdvertiseIsEnabled());
          }
      }
  
      /**
       * @return whether battery saver policy is enabled or not. This does not take into account
       * whether a policy says to advertise isEnabled, so this shouldn't be propagated externally.
       */
      private boolean isPolicyEnabled() {
          synchronized (mLock) {
              return mFullEnabled || mAdaptiveEnabled;
          }
      }
  
      boolean isFullEnabled() {
          synchronized (mLock) {
              return mFullEnabled;
          }
      }
  
      boolean isAdaptiveEnabled() {
          synchronized (mLock) {
              return mAdaptiveEnabled;
          }
      }
  
      boolean setAdaptivePolicyLocked(String settings, String deviceSpecificSettings, int reason) {
          return setAdaptivePolicyLocked(
                  BatterySaverPolicy.Policy.fromSettings(settings, deviceSpecificSettings),
                  reason);
      }
  
      boolean setAdaptivePolicyLocked(BatterySaverPolicyConfig config, int reason) {
          return setAdaptivePolicyLocked(BatterySaverPolicy.Policy.fromConfig(config), reason);
      }
  
      boolean setAdaptivePolicyLocked(Policy policy, int reason) {
          if (mBatterySaverPolicy.setAdaptivePolicyLocked(policy)) {
              mHandler.postStateChanged(/*sendBroadcast=*/ true, reason);
              return true;
          }
          return false;
      }
  
      boolean resetAdaptivePolicyLocked(int reason) {
          if (mBatterySaverPolicy.resetAdaptivePolicyLocked()) {
              mHandler.postStateChanged(/*sendBroadcast=*/ true, reason);
              return true;
          }
          return false;
      }
  
      boolean setAdaptivePolicyEnabledLocked(boolean enabled, int reason) {
          if (mAdaptiveEnabled == enabled) {
              return false;
          }
          mAdaptiveEnabled = enabled;
          if (updatePolicyLevelLocked()) {
              mHandler.postStateChanged(/*sendBroadcast=*/ true, reason);
              return true;
          }
          return false;
      }
  
      /** @return whether device is in interactive state. */
      public boolean isInteractive() {
          synchronized (mLock) {
              return mIsInteractive;
          }
      }
  
      /** @return Battery saver policy. */
      public BatterySaverPolicy getBatterySaverPolicy() {
          return mBatterySaverPolicy;
      }
  
      /**
       * @return true if launch boost should currently be disabled.
       */
      public boolean isLaunchBoostDisabled() {
          return isPolicyEnabled() && mBatterySaverPolicy.isLaunchBoostDisabled();
      }
  
      /**
       * Dispatch power save events to the listeners.
       *
       * This method is always called on the handler thread.
       *
       * This method is called only in the following cases:
       * - When battery saver becomes activated.
       * - When battery saver becomes deactivated.
       * - When battery saver is on and the interactive state changes.
       * - When battery saver is on and the battery saver policy changes.
       * - When adaptive battery saver becomes activated.
       * - When adaptive battery saver becomes deactivated.
       * - When adaptive battery saver is active (and full is off) and the policy changes.
       */
      void handleBatterySaverStateChanged(boolean sendBroadcast, int reason) {
          final LowPowerModeListener[] listeners;
  
          final boolean enabled;
          final boolean isInteractive = getPowerManager().isInteractive();
          final ArrayMap<String, String> fileValues;
  
          synchronized (mLock) {
              enabled = mFullEnabled || mAdaptiveEnabled;
  
              EventLogTags.writeBatterySaverMode(
                      mFullPreviouslyEnabled ? 1 : 0, // Previously off or on.
                      mAdaptivePreviouslyEnabled ? 1 : 0, // Previously off or on.
                      mFullEnabled ? 1 : 0, // Now off or on.
                      mAdaptiveEnabled ? 1 : 0, // Now off or on.
                      isInteractive ?  1 : 0, // Device interactive state.
                      enabled ? mBatterySaverPolicy.toEventLogString() : "",
                      reason);
  
              mFullPreviouslyEnabled = mFullEnabled;
              mAdaptivePreviouslyEnabled = mAdaptiveEnabled;
  
              listeners = mListeners.toArray(new LowPowerModeListener[0]);
  
              mIsInteractive = isInteractive;
  
              if (enabled) {
                  fileValues = mBatterySaverPolicy.getFileValues(isInteractive);
              } else {
                  fileValues = null;
              }
          }
  
          final PowerManagerInternal pmi = LocalServices.getService(PowerManagerInternal.class);
          if (pmi != null) {
              pmi.powerHint(PowerHint.LOW_POWER, isEnabled() ? 1 : 0);
          }
  
          updateBatterySavingStats();
  
          if (ArrayUtils.isEmpty(fileValues)) {
              mFileUpdater.restoreDefault();
          } else {
              mFileUpdater.writeFiles(fileValues);
          }
  
          for (Plugin p : mPlugins) {
              p.onBatterySaverChanged(this);
          }
  
          if (sendBroadcast) {
  
              if (DEBUG) {
                  Slog.i(TAG, "Sending broadcasts for mode: " + isEnabled());
              }
  
              // Send the broadcasts and notify the listeners. We only do this when the battery saver
              // mode changes, but not when only the screen state changes.
              Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING)
                      .putExtra(PowerManager.EXTRA_POWER_SAVE_MODE, isEnabled())
                      .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
              mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
  
              intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
              intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
              mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
  
              // Send internal version that requires signature permission.
              intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL);
              intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
              mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
                      Manifest.permission.DEVICE_POWER);
  
              for (LowPowerModeListener listener : listeners) {
                  final PowerSaveState result =
                          mBatterySaverPolicy.getBatterySaverPolicy(listener.getServiceType());
                  listener.onLowPowerModeChanged(result);
              }
          }
      }
  
      private void updateBatterySavingStats() {
          final PowerManager pm = getPowerManager();
          if (pm == null) {
              Slog.wtf(TAG, "PowerManager not initialized");
              return;
          }
          final boolean isInteractive = pm.isInteractive();
          final int dozeMode =
                  pm.isDeviceIdleMode() ? DozeState.DEEP
                          : pm.isLightDeviceIdleMode() ? DozeState.LIGHT
                          : DozeState.NOT_DOZING;
  
          synchronized (mLock) {
              if (mIsPluggedIn) {
                  mBatterySavingStats.startCharging();
                  return;
              }
              mBatterySavingStats.transitionState(
                      mFullEnabled ? BatterySaverState.ON :
                              (mAdaptiveEnabled ? BatterySaverState.ADAPTIVE : BatterySaverState.OFF),
                      isInteractive ? InteractiveState.INTERACTIVE : InteractiveState.NON_INTERACTIVE,
                      dozeMode);
          }
      }
  }
  