/*
|
* 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));
|
}
|
}
|