summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Makoto Onuki <omakoto@google.com> 2018-01-26 10:26:36 -0800
committer Makoto Onuki <omakoto@google.com> 2018-01-29 11:09:14 -0800
commit076218bfc0094c17f95c2e8afa4d5b2480f03f73 (patch)
treee66bf0248b8b5af1d35a33a047eeb3c3874fa841
parent84783905c03d368352fbd05dfb614251a72f1786 (diff)
Keep track of battery drain rate in various states...
... and print in dumpsys power. Bug: 72229630 Test: manual test with dumpsys power, etc Test: atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java Change-Id: I43949129ff03c1e0b0fa3aa603e0678b728538ee
-rw-r--r--core/java/android/os/BatteryManagerInternal.java37
-rw-r--r--services/core/java/com/android/server/BatteryService.java17
-rw-r--r--services/core/java/com/android/server/EventLogTags.logtags1
-rw-r--r--services/core/java/com/android/server/power/BatterySaverPolicy.java3
-rw-r--r--services/core/java/com/android/server/power/batterysaver/BatterySaverController.java54
-rw-r--r--services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java353
-rw-r--r--services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java205
7 files changed, 668 insertions, 2 deletions
diff --git a/core/java/android/os/BatteryManagerInternal.java b/core/java/android/os/BatteryManagerInternal.java
index f3a95b90d5c1..a86237dd271f 100644
--- a/core/java/android/os/BatteryManagerInternal.java
+++ b/core/java/android/os/BatteryManagerInternal.java
@@ -24,26 +24,63 @@ package android.os;
public abstract class BatteryManagerInternal {
/**
* Returns true if the device is plugged into any of the specified plug types.
+ *
+ * This is a simple accessor that's safe to be called from any locks, but internally it may
+ * wait on the battery service lock.
*/
public abstract boolean isPowered(int plugTypeSet);
/**
* Returns the current plug type.
+ *
+ * This is a simple accessor that's safe to be called from any locks, but internally it may
+ * wait on the battery service lock.
*/
public abstract int getPlugType();
/**
* Returns battery level as a percentage.
+ *
+ * This is a simple accessor that's safe to be called from any locks, but internally it may
+ * wait on the battery service lock.
*/
public abstract int getBatteryLevel();
/**
+ * Instantaneous battery capacity in uA-h, as defined in the HealthInfo HAL struct.
+ * Please note apparently it could be bigger than {@link #getBatteryFullCharge}.
+ *
+ * This is a simple accessor that's safe to be called from any locks, but internally it may
+ * wait on the battery service lock.
+ *
+ * @see android.hardware.health.V1_0.HealthInfo#batteryChargeCounter
+ */
+ public abstract int getBatteryChargeCounter();
+
+ /**
+ * Battery charge value when it is considered to be "full" in uA-h , as defined in the
+ * HealthInfo HAL struct.
+ *
+ * This is a simple accessor that's safe to be called from any locks, but internally it may
+ * wait on the battery service lock.
+ *
+ * @see android.hardware.health.V1_0.HealthInfo#batteryFullCharge
+ */
+ public abstract int getBatteryFullCharge();
+
+ /**
* Returns whether we currently consider the battery level to be low.
+ *
+ * This is a simple accessor that's safe to be called from any locks, but internally it may
+ * wait on the battery service lock.
*/
public abstract boolean getBatteryLevelLow();
/**
* Returns a non-zero value if an unsupported charger is attached.
+ *
+ * This is a simple accessor that's safe to be called from any locks, but internally it may
+ * wait on the battery service lock.
*/
public abstract int getInvalidCharger();
}
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index ba9883b4de59..8265262d3824 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -835,6 +835,9 @@ public final class BatteryService extends SystemService {
case "level":
mHealthInfo.batteryLevel = Integer.parseInt(value);
break;
+ case "counter":
+ mHealthInfo.batteryChargeCounter = Integer.parseInt(value);
+ break;
case "temp":
mHealthInfo.batteryTemperature = Integer.parseInt(value);
break;
@@ -1164,6 +1167,20 @@ public final class BatteryService extends SystemService {
}
@Override
+ public int getBatteryChargeCounter() {
+ synchronized (mLock) {
+ return mHealthInfo.batteryChargeCounter;
+ }
+ }
+
+ @Override
+ public int getBatteryFullCharge() {
+ synchronized (mLock) {
+ return mHealthInfo.batteryFullCharge;
+ }
+ }
+
+ @Override
public boolean getBatteryLevelLow() {
synchronized (mLock) {
return mBatteryLevelLow;
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index 732ac66b41de..219facd0b002 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -34,6 +34,7 @@ option java_package com.android.server
2731 power_soft_sleep_requested (savedwaketimems|2)
# Power save state has changed. See BatterySaverController.java for the details.
2739 battery_saver_mode (prevOffOrOn|1|5),(nowOffOrOn|1|5),(interactive|1|5),(features|3|5)
+27390 battery_saving_stats (batterySaver|1|5),(interactive|1|5),(doze|1|5),(delta_duration|2|3),(delta_battery_drain|1|6),(total_duration|2|3),(total_battery_drain|1|6)
#
# Leave IDs through 2740 for more power logs (2730 used by battery_discharge above)
diff --git a/services/core/java/com/android/server/power/BatterySaverPolicy.java b/services/core/java/com/android/server/power/BatterySaverPolicy.java
index a538967e501d..847c90a08c5c 100644
--- a/services/core/java/com/android/server/power/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/BatterySaverPolicy.java
@@ -33,6 +33,7 @@ import android.util.Slog;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.power.batterysaver.BatterySavingStats;
import com.android.server.power.batterysaver.CpuFrequencies;
import java.io.PrintWriter;
@@ -498,6 +499,8 @@ public class BatterySaverPolicy extends ContentObserver {
pw.print(" Noninteractive File values:\n");
dumpMap(pw, " ", mFilesForNoninteractive);
pw.println();
+ pw.println();
+ BatterySavingStats.getInstance().dump(pw, " ");
}
}
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
index d4627c2da07e..32f38b7cf772 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
@@ -28,6 +28,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.power.V1_0.PowerHint;
import android.net.Uri;
+import android.os.BatteryManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -50,6 +51,9 @@ import com.android.server.LocalServices;
import com.android.server.power.BatterySaverPolicy;
import com.android.server.power.BatterySaverPolicy.BatterySaverPolicyListener;
import com.android.server.power.PowerManagerService;
+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;
@@ -70,6 +74,8 @@ public class BatterySaverController implements BatterySaverPolicyListener {
private final BatterySaverPolicy mBatterySaverPolicy;
+ private final BatterySavingStats mBatterySavingStats;
+
private static final String WARNING_LINK_URL = "http://goto.google.com/extreme-battery-saver";
@GuardedBy("mLock")
@@ -78,6 +84,9 @@ public class BatterySaverController implements BatterySaverPolicyListener {
@GuardedBy("mLock")
private boolean mEnabled;
+ @GuardedBy("mLock")
+ private boolean mIsPluggedIn;
+
/**
* Previously enabled or not; only for the event logging. Only use it from
* {@link #handleBatterySaverStateChanged}.
@@ -104,15 +113,28 @@ public class BatterySaverController implements BatterySaverPolicyListener {
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 (!isEnabled()) {
+ 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);
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;
}
}
};
@@ -126,6 +148,7 @@ public class BatterySaverController implements BatterySaverPolicyListener {
mBatterySaverPolicy = policy;
mBatterySaverPolicy.addListener(this);
mFileUpdater = new FileUpdater(context);
+ mBatterySavingStats = BatterySavingStats.getInstance();
// Initialize plugins.
final ArrayList<Plugin> plugins = new ArrayList<>();
@@ -149,6 +172,9 @@ public class BatterySaverController implements BatterySaverPolicyListener {
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)
@@ -280,7 +306,6 @@ public class BatterySaverController implements BatterySaverPolicyListener {
enabled = mEnabled;
mIsInteractive = isInteractive;
-
if (enabled) {
fileValues = mBatterySaverPolicy.getFileValues(isInteractive);
} else {
@@ -293,6 +318,8 @@ public class BatterySaverController implements BatterySaverPolicyListener {
pmi.powerHint(PowerHint.LOW_POWER, enabled ? 1 : 0);
}
+ updateBatterySavingStats();
+
if (ArrayUtils.isEmpty(fileValues)) {
mFileUpdater.restoreDefault();
} else {
@@ -332,7 +359,6 @@ public class BatterySaverController implements BatterySaverPolicyListener {
mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
Manifest.permission.DEVICE_POWER);
-
for (LowPowerModeListener listener : listeners) {
final PowerSaveState result =
mBatterySaverPolicy.getBatterySaverPolicy(
@@ -388,4 +414,28 @@ public class BatterySaverController implements BatterySaverPolicyListener {
foregroundUser);
}
}
+
+ 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(
+ mEnabled ? BatterySaverState.ON : BatterySaverState.OFF,
+ isInteractive ? InteractiveState.INTERACTIVE : InteractiveState.NON_INTERACTIVE,
+ dozeMode);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java b/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java
new file mode 100644
index 000000000000..df4f8eca5595
--- /dev/null
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySavingStats.java
@@ -0,0 +1,353 @@
+/*
+ * 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 com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+import com.android.server.power.BatterySaverPolicy;
+
+import java.io.PrintWriter;
+
+/**
+ * This class keeps track of battery drain rate.
+ *
+ * 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 = new Object();
+
+ /** Whether battery saver is on or off. */
+ interface BatterySaverState {
+ int OFF = 0;
+ int ON = 1;
+
+ int SHIFT = 0;
+ int BITS = 1;
+ 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 long totalTimeMillis;
+ public int totalBatteryDrain;
+
+ public long totalMinutes() {
+ return totalTimeMillis / 60_000;
+ }
+
+ public double drainPerHour() {
+ if (totalTimeMillis == 0) {
+ return 0;
+ }
+ return (double) totalBatteryDrain / (totalTimeMillis / (60.0 * 60 * 1000));
+ }
+
+ @VisibleForTesting
+ String toStringForTest() {
+ return "{" + totalMinutes() + "m," + totalBatteryDrain + ","
+ + String.format("%.2f", drainPerHour()) + "}";
+ }
+ }
+
+ private static BatterySavingStats sInstance;
+
+ 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<>();
+
+ /**
+ * Don't call it directly -- use {@link #getInstance()}. Not private for testing.
+ */
+ @VisibleForTesting
+ BatterySavingStats() {
+ mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
+ }
+
+ public static synchronized BatterySavingStats getInstance() {
+ if (sInstance == null) {
+ sInstance = new BatterySavingStats();
+ }
+ return sInstance;
+ }
+
+ private BatteryManagerInternal getBatteryManagerInternal() {
+ if (mBatteryManagerInternal == null) {
+ mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
+ }
+ 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));
+ }
+
+ long injectCurrentTime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ int injectBatteryLevel() {
+ final BatteryManagerInternal bmi = getBatteryManagerInternal();
+ if (bmi == null) {
+ Slog.wtf(TAG, "BatteryManagerInternal not initialized");
+ return 0;
+ }
+ return bmi.getBatteryChargeCounter();
+ }
+
+ /**
+ * 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);
+ if (mCurrentState == newState) {
+ return;
+ }
+
+ endLastStateLocked();
+ startNewStateLocked(newState);
+ }
+ }
+
+ /**
+ * Called from the outside when the device is plugged in.
+ */
+ public void startCharging() {
+ synchronized (mLock) {
+ if (mCurrentState < 0) {
+ return;
+ }
+
+ endLastStateLocked();
+ startNewStateLocked(STATE_CHARGING);
+ }
+ }
+
+ private void endLastStateLocked() {
+ if (mCurrentState < 0) {
+ return;
+ }
+ final Stat stat = getStat(mCurrentState);
+
+ stat.endBatteryLevel = injectBatteryLevel();
+ stat.endTime = injectCurrentTime();
+
+ final long deltaTime = stat.endTime - stat.startTime;
+ final int deltaDrain = stat.startBatteryLevel - stat.endBatteryLevel;
+
+ stat.totalTimeMillis += deltaTime;
+ stat.totalBatteryDrain += deltaDrain;
+
+ if (DEBUG) {
+ Slog.d(TAG, "State summary: " + stateToString(mCurrentState)
+ + ": " + (deltaTime / 1_000) + "s "
+ + "Start level: " + stat.startBatteryLevel + "uA "
+ + "End level: " + stat.endBatteryLevel + "uA "
+ + deltaDrain + "uA");
+ }
+ EventLogTags.writeBatterySavingStats(
+ BatterySaverState.fromIndex(mCurrentState),
+ InteractiveState.fromIndex(mCurrentState),
+ DozeState.fromIndex(mCurrentState),
+ deltaTime,
+ deltaDrain,
+ stat.totalTimeMillis,
+ stat.totalBatteryDrain);
+ }
+
+ private void startNewStateLocked(int newState) {
+ if (DEBUG) {
+ Slog.d(TAG, "New state: " + stateToString(newState));
+ }
+ mCurrentState = newState;
+
+ if (mCurrentState < 0) {
+ return;
+ }
+
+ final Stat stat = getStat(mCurrentState);
+ stat.startBatteryLevel = injectBatteryLevel();
+ stat.startTime = injectCurrentTime();
+ stat.endTime = 0;
+ }
+
+ public void dump(PrintWriter pw, String indent) {
+ synchronized (mLock) {
+ pw.print(indent);
+ pw.println("Battery Saving Stats:");
+
+ indent = indent + " ";
+
+ 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, " ");
+
+ pw.println();
+ }
+ }
+
+ 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 %6dmA %8.1fmA/h %6dm %6dmA %8.1fmA/h",
+ offStat.totalMinutes(),
+ offStat.totalBatteryDrain / 1000,
+ offStat.drainPerHour() / 1000.0,
+ onStat.totalMinutes(),
+ onStat.totalBatteryDrain / 1000,
+ onStat.drainPerHour() / 1000.0));
+ }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java
new file mode 100644
index 000000000000..f788caccbdce
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.power.batterysaver;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+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 org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+/**
+ atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BatterySavingStatsTest {
+ private class BatterySavingStatsTestable extends BatterySavingStats {
+ private long mTime = 1_000_000; // Some random starting time.
+
+ private int mBatteryLevel = 100;
+
+ @Override
+ long injectCurrentTime() {
+ return mTime;
+ }
+
+ @Override
+ int injectBatteryLevel() {
+ return mBatteryLevel;
+ }
+
+ void assertDumpable() {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ dump(new PrintWriter(out), ""); // Just make sure it won't crash.
+ }
+
+ void advanceClock(int minutes) {
+ mTime += 60_000 * minutes;
+ }
+
+ void drainBattery(int percent) {
+ mBatteryLevel -= percent;
+ if (mBatteryLevel < 0) {
+ mBatteryLevel = 0;
+ }
+ }
+
+ String toDebugString() {
+ final StringBuilder sb = new StringBuilder();
+ String sep = "";
+ for (int i = 0; i < mStats.size(); i++) {
+ sb.append(sep);
+ sb.append(stateToString(mStats.keyAt(i)));
+ sb.append(":");
+ sb.append(mStats.valueAt(i).toStringForTest());
+ sep = "\n";
+ }
+ return sb.toString();
+ }
+ }
+
+ @Test
+ public void testAll() {
+ final BatterySavingStatsTestable target = new BatterySavingStatsTestable();
+
+ target.assertDumpable();
+
+ target.advanceClock(1);
+ target.drainBattery(2);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(4);
+ target.drainBattery(1);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.NON_INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(2);
+ target.drainBattery(5);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(4);
+ target.drainBattery(1);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.NON_INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(2);
+ target.drainBattery(5);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(3);
+ target.drainBattery(1);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.NON_INTERACTIVE,
+ DozeState.LIGHT);
+
+ target.advanceClock(5);
+ target.drainBattery(1);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.NON_INTERACTIVE,
+ DozeState.DEEP);
+
+ target.advanceClock(1);
+ target.drainBattery(2);
+
+ target.transitionState(
+ BatterySaverState.ON,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(1);
+ target.drainBattery(3);
+
+ target.transitionState(
+ BatterySaverState.OFF,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(3);
+ target.drainBattery(5);
+
+ target.transitionState(
+ BatterySaverState.ON,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(3);
+ target.drainBattery(5);
+
+ target.startCharging();
+
+ target.advanceClock(5);
+ target.drainBattery(10);
+
+ target.transitionState(
+ BatterySaverState.ON,
+ InteractiveState.INTERACTIVE,
+ DozeState.NOT_DOZING);
+
+ target.advanceClock(5);
+ target.drainBattery(1);
+
+ target.startCharging();
+
+ target.assertDumpable();
+
+ assertEquals(
+ "BS=0,I=0,D=0:{4m,10,150.00}\n" +
+ "BS=1,I=0,D=0:{0m,0,0.00}\n" +
+ "BS=0,I=1,D=0:{14m,8,34.29}\n" +
+ "BS=1,I=1,D=0:{9m,9,60.00}\n" +
+ "BS=0,I=0,D=1:{5m,1,12.00}\n" +
+ "BS=1,I=0,D=1:{0m,0,0.00}\n" +
+ "BS=0,I=1,D=1:{0m,0,0.00}\n" +
+ "BS=1,I=1,D=1:{0m,0,0.00}\n" +
+ "BS=0,I=0,D=2:{1m,2,120.00}\n" +
+ "BS=1,I=0,D=2:{0m,0,0.00}\n" +
+ "BS=0,I=1,D=2:{0m,0,0.00}\n" +
+ "BS=1,I=1,D=2:{0m,0,0.00}",
+ target.toDebugString());
+ }
+}