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 android.os.BatteryManagerInternal;
  import android.os.SystemClock;
  import android.util.ArrayMap;
  import android.util.Slog;
  import android.util.TimeUtils;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.server.EventLogTags;
  import com.android.server.LocalServices;
  
  import java.io.PrintWriter;
  import java.text.SimpleDateFormat;
  import java.util.Date;
  
  /**
   * This class keeps track of battery drain rate.
   *
   * 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.)
   *
   * TODO: The use of the terms "percent" and "level" in this class is not standard. Fix it.
   *
   * Test:
   atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java
   */
  public class BatterySavingStats {
  
      private static final String TAG = "BatterySavingStats";
  
      private static final boolean DEBUG = BatterySaverPolicy.DEBUG;
  
      private final Object mLock;
  
      /** Whether battery saver is on or off. */
      interface BatterySaverState {
          int OFF = 0;
          int ON = 1;
          int ADAPTIVE = 2;
  
          int SHIFT = 0;
          int BITS = 2;
          int MASK = (1 << BITS) - 1;
  
          static int fromIndex(int index) {
              return (index >> SHIFT) & MASK;
          }
      }
  
      /** Whether the device is interactive (i.e. screen on) or not. */
      interface InteractiveState {
          int NON_INTERACTIVE = 0;
          int INTERACTIVE = 1;
  
          int SHIFT = BatterySaverState.SHIFT + BatterySaverState.BITS;
          int BITS = 1;
          int MASK = (1 << BITS) - 1;
  
          static int fromIndex(int index) {
              return (index >> SHIFT) & MASK;
          }
      }
  
      /** Doze mode. */
      interface DozeState {
          int NOT_DOZING = 0;
          int LIGHT = 1;
          int DEEP = 2;
  
          int SHIFT = InteractiveState.SHIFT + InteractiveState.BITS;
          int BITS = 2;
          int MASK = (1 << BITS) - 1;
  
          static int fromIndex(int index) {
              return (index >> SHIFT) & MASK;
          }
      }
  
      /**
       * Various stats in each state.
       */
      static class Stat {
          public long startTime;
          public long endTime;
  
          public int startBatteryLevel;
          public int endBatteryLevel;
  
          public int startBatteryPercent;
          public int endBatteryPercent;
  
          public long totalTimeMillis;
          public int totalBatteryDrain;
          public int totalBatteryDrainPercent;
  
          public long totalMinutes() {
              return totalTimeMillis / 60_000;
          }
  
          public double drainPerHour() {
              if (totalTimeMillis == 0) {
                  return 0;
              }
              return (double) totalBatteryDrain / (totalTimeMillis / (60.0 * 60 * 1000));
          }
  
          public double drainPercentPerHour() {
              if (totalTimeMillis == 0) {
                  return 0;
              }
              return (double) totalBatteryDrainPercent / (totalTimeMillis / (60.0 * 60 * 1000));
          }
  
          @VisibleForTesting
          String toStringForTest() {
              return "{" + totalMinutes() + "m," + totalBatteryDrain + ","
                      + String.format("%.2f", drainPerHour()) + "uA/H,"
                      + String.format("%.2f", drainPercentPerHour()) + "%"
                      + "}";
          }
      }
  
      private BatteryManagerInternal mBatteryManagerInternal;
  
      private static final int STATE_NOT_INITIALIZED = -1;
      private static final int STATE_CHARGING = -2;
  
      /**
       * Current state, one of STATE_* or values returned by {@link #statesToIndex}.
       */
      @GuardedBy("mLock")
      private int mCurrentState = STATE_NOT_INITIALIZED;
  
      /**
       * Stats in each state.
       */
      @VisibleForTesting
      @GuardedBy("mLock")
      final ArrayMap<Integer, Stat> mStats = new ArrayMap<>();
  
      @GuardedBy("mLock")
      private int mBatterySaverEnabledCount = 0;
  
      @GuardedBy("mLock")
      private boolean mIsBatterySaverEnabled;
  
      @GuardedBy("mLock")
      private long mLastBatterySaverEnabledTime = 0;
  
      @GuardedBy("mLock")
      private long mLastBatterySaverDisabledTime = 0;
  
      /** Visible for unit tests */
      @VisibleForTesting
      public BatterySavingStats(Object lock) {
          mLock = lock;
          mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
      }
  
      private BatteryManagerInternal getBatteryManagerInternal() {
          if (mBatteryManagerInternal == null) {
              mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
              if (mBatteryManagerInternal == null) {
                  Slog.wtf(TAG, "BatteryManagerInternal not initialized");
              }
          }
          return mBatteryManagerInternal;
      }
  
      /**
       * Takes a state triplet and generates a state index.
       */
      @VisibleForTesting
      static int statesToIndex(
              int batterySaverState, int interactiveState, int dozeState) {
          int ret = batterySaverState & BatterySaverState.MASK;
          ret |= (interactiveState & InteractiveState.MASK) << InteractiveState.SHIFT;
          ret |= (dozeState & DozeState.MASK) << DozeState.SHIFT;
          return ret;
      }
  
      /**
       * Takes a state index and returns a string for logging.
       */
      @VisibleForTesting
      static String stateToString(int state) {
          switch (state) {
              case STATE_NOT_INITIALIZED:
                  return "NotInitialized";
              case STATE_CHARGING:
                  return "Charging";
          }
          return "BS=" + BatterySaverState.fromIndex(state)
                  + ",I=" + InteractiveState.fromIndex(state)
                  + ",D=" + DozeState.fromIndex(state);
      }
  
      /**
       * @return {@link Stat} fo a given state.
       */
      @VisibleForTesting
      Stat getStat(int stateIndex) {
          synchronized (mLock) {
              Stat stat = mStats.get(stateIndex);
              if (stat == null) {
                  stat = new Stat();
                  mStats.put(stateIndex, stat);
              }
              return stat;
          }
      }
  
      /**
       * @return {@link Stat} fo a given state triplet.
       */
      private Stat getStat(int batterySaverState, int interactiveState, int dozeState) {
          return getStat(statesToIndex(batterySaverState, interactiveState, dozeState));
      }
  
      @VisibleForTesting
      long injectCurrentTime() {
          return SystemClock.elapsedRealtime();
      }
  
      @VisibleForTesting
      int injectBatteryLevel() {
          final BatteryManagerInternal bmi = getBatteryManagerInternal();
          if (bmi == null) {
              return 0;
          }
          return bmi.getBatteryChargeCounter();
      }
  
      @VisibleForTesting
      int injectBatteryPercent() {
          final BatteryManagerInternal bmi = getBatteryManagerInternal();
          if (bmi == null) {
              return 0;
          }
          return bmi.getBatteryLevel();
      }
  
      /**
       * Called from the outside whenever any of the states changes, when the device is not plugged
       * in.
       */
      public void transitionState(int batterySaverState, int interactiveState, int dozeState) {
          synchronized (mLock) {
              final int newState = statesToIndex(
                      batterySaverState, interactiveState, dozeState);
              transitionStateLocked(newState);
          }
      }
  
      /**
       * Called from the outside when the device is plugged in.
       */
      public void startCharging() {
          synchronized (mLock) {
              transitionStateLocked(STATE_CHARGING);
          }
      }
  
      @GuardedBy("mLock")
      private void transitionStateLocked(int newState) {
          if (mCurrentState == newState) {
              return;
          }
          final long now = injectCurrentTime();
          final int batteryLevel = injectBatteryLevel();
          final int batteryPercent = injectBatteryPercent();
  
          final boolean oldBatterySaverEnabled =
                  BatterySaverState.fromIndex(mCurrentState) != BatterySaverState.OFF;
          final boolean newBatterySaverEnabled =
                  BatterySaverState.fromIndex(newState) != BatterySaverState.OFF;
          if (oldBatterySaverEnabled != newBatterySaverEnabled) {
              mIsBatterySaverEnabled = newBatterySaverEnabled;
              if (newBatterySaverEnabled) {
                  mBatterySaverEnabledCount++;
                  mLastBatterySaverEnabledTime = injectCurrentTime();
              } else {
                  mLastBatterySaverDisabledTime = injectCurrentTime();
              }
          }
  
          endLastStateLocked(now, batteryLevel, batteryPercent);
          startNewStateLocked(newState, now, batteryLevel, batteryPercent);
      }
  
      @GuardedBy("mLock")
      private void endLastStateLocked(long now, int batteryLevel, int batteryPercent) {
          if (mCurrentState < 0) {
              return;
          }
          final Stat stat = getStat(mCurrentState);
  
          stat.endBatteryLevel = batteryLevel;
          stat.endBatteryPercent = batteryPercent;
          stat.endTime = now;
  
          final long deltaTime = stat.endTime - stat.startTime;
          final int deltaDrain = stat.startBatteryLevel - stat.endBatteryLevel;
          final int deltaPercent = stat.startBatteryPercent - stat.endBatteryPercent;
  
          stat.totalTimeMillis += deltaTime;
          stat.totalBatteryDrain += deltaDrain;
          stat.totalBatteryDrainPercent += deltaPercent;
  
          if (DEBUG) {
              Slog.d(TAG, "State summary: " + stateToString(mCurrentState)
                      + ": " + (deltaTime / 1_000) + "s "
                      + "Start level: " + stat.startBatteryLevel + "uA "
                      + "End level: " + stat.endBatteryLevel + "uA "
                      + "Start percent: " + stat.startBatteryPercent + "% "
                      + "End percent: " + stat.endBatteryPercent + "% "
                      + "Drain " + deltaDrain + "uA");
          }
          EventLogTags.writeBatterySavingStats(
                  BatterySaverState.fromIndex(mCurrentState),
                  InteractiveState.fromIndex(mCurrentState),
                  DozeState.fromIndex(mCurrentState),
                  deltaTime,
                  deltaDrain,
                  deltaPercent,
                  stat.totalTimeMillis,
                  stat.totalBatteryDrain,
                  stat.totalBatteryDrainPercent);
  
      }
  
      @GuardedBy("mLock")
      private void startNewStateLocked(int newState, long now, int batteryLevel, int batteryPercent) {
          if (DEBUG) {
              Slog.d(TAG, "New state: " + stateToString(newState));
          }
          mCurrentState = newState;
  
          if (mCurrentState < 0) {
              return;
          }
  
          final Stat stat = getStat(mCurrentState);
          stat.startBatteryLevel = batteryLevel;
          stat.startBatteryPercent = batteryPercent;
          stat.startTime = now;
          stat.endTime = 0;
      }
  
      public void dump(PrintWriter pw, String indent) {
          synchronized (mLock) {
              pw.print(indent);
              pw.println("Battery saving stats:");
  
              indent = indent + "  ";
  
              final long now = System.currentTimeMillis();
              final long nowElapsed = injectCurrentTime();
              final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
  
              pw.print(indent);
              pw.print("Battery Saver is currently: ");
              pw.println(mIsBatterySaverEnabled ? "ON" : "OFF");
              if (mLastBatterySaverEnabledTime > 0) {
                  pw.print(indent);
                  pw.print("  ");
                  pw.print("Last ON time: ");
                  pw.print(sdf.format(new Date(now - nowElapsed + mLastBatterySaverEnabledTime)));
                  pw.print(" ");
                  TimeUtils.formatDuration(mLastBatterySaverEnabledTime, nowElapsed, pw);
                  pw.println();
              }
  
              if (mLastBatterySaverDisabledTime > 0) {
                  pw.print(indent);
                  pw.print("  ");
                  pw.print("Last OFF time: ");
                  pw.print(sdf.format(new Date(now - nowElapsed + mLastBatterySaverDisabledTime)));
                  pw.print(" ");
                  TimeUtils.formatDuration(mLastBatterySaverDisabledTime, nowElapsed, pw);
                  pw.println();
              }
  
              pw.print(indent);
              pw.print("  ");
              pw.print("Times enabled: ");
              pw.println(mBatterySaverEnabledCount);
  
              pw.println();
  
              pw.print(indent);
              pw.println("Drain stats:");
  
              pw.print(indent);
              pw.println("                   Battery saver OFF                          ON");
              dumpLineLocked(pw, indent, InteractiveState.NON_INTERACTIVE, "NonIntr",
                      DozeState.NOT_DOZING, "NonDoze");
              dumpLineLocked(pw, indent, InteractiveState.INTERACTIVE, "   Intr",
                      DozeState.NOT_DOZING, "       ");
  
              dumpLineLocked(pw, indent, InteractiveState.NON_INTERACTIVE, "NonIntr",
                      DozeState.DEEP, "Deep   ");
              dumpLineLocked(pw, indent, InteractiveState.INTERACTIVE, "   Intr",
                      DozeState.DEEP, "       ");
  
              dumpLineLocked(pw, indent, InteractiveState.NON_INTERACTIVE, "NonIntr",
                      DozeState.LIGHT, "Light  ");
              dumpLineLocked(pw, indent, InteractiveState.INTERACTIVE, "   Intr",
                      DozeState.LIGHT, "       ");
          }
      }
  
      private void dumpLineLocked(PrintWriter pw, String indent,
              int interactiveState, String interactiveLabel,
              int dozeState, String dozeLabel) {
          pw.print(indent);
          pw.print(dozeLabel);
          pw.print(" ");
          pw.print(interactiveLabel);
          pw.print(": ");
  
          final Stat offStat = getStat(BatterySaverState.OFF, interactiveState, dozeState);
          final Stat onStat = getStat(BatterySaverState.ON, interactiveState, dozeState);
  
          pw.println(String.format("%6dm %6dmAh(%3d%%) %8.1fmAh/h     %6dm %6dmAh(%3d%%) %8.1fmAh/h",
                  offStat.totalMinutes(),
                  offStat.totalBatteryDrain / 1000,
                  offStat.totalBatteryDrainPercent,
                  offStat.drainPerHour() / 1000.0,
                  onStat.totalMinutes(),
                  onStat.totalBatteryDrain / 1000,
                  onStat.totalBatteryDrainPercent,
                  onStat.drainPerHour() / 1000.0));
      }
  }
  