Merge changes from topic "cherrypick-config_batteryStatsResetOnUnplugHighBatteryLevel-zmiab0332q" into tm-qpr-dev
* changes:
Reset BatteryStats when device has been plugged in for a long time.
Add config to control BatteryStats reset logic
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 6fed26c..3a0e09d 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -81,6 +81,7 @@
import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
@@ -201,6 +202,7 @@
public static final int RESET_REASON_ADB_COMMAND = 2;
public static final int RESET_REASON_FULL_CHARGE = 3;
public static final int RESET_REASON_MEASURED_ENERGY_BUCKETS_CHANGE = 4;
+ public static final int RESET_REASON_PLUGGED_IN_FOR_LONG_DURATION = 5;
protected Clock mClock;
@@ -387,6 +389,89 @@
}
}
+ /** Provide BatteryStatsImpl configuration choices */
+ public static class BatteryStatsConfig {
+ static final int RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG = 1 << 0;
+ static final int RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG = 1 << 1;
+
+ private final int mFlags;
+
+ private BatteryStatsConfig(Builder builder) {
+ int flags = 0;
+ if (builder.mResetOnUnplugHighBatteryLevel) {
+ flags |= RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG;
+ }
+ if (builder.mResetOnUnplugAfterSignificantCharge) {
+ flags |= RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
+ }
+ mFlags = flags;
+ }
+
+ /**
+ * Returns whether a BatteryStats reset should occur on unplug when the battery level is
+ * high.
+ */
+ boolean shouldResetOnUnplugHighBatteryLevel() {
+ return (mFlags & RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG)
+ == RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG;
+ }
+
+ /**
+ * Returns whether a BatteryStats reset should occur on unplug if the battery charge a
+ * significant amount since it has been plugged in.
+ */
+ boolean shouldResetOnUnplugAfterSignificantCharge() {
+ return (mFlags & RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG)
+ == RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
+ }
+
+ /**
+ * Builder for BatteryStatsConfig
+ */
+ public static class Builder {
+ private boolean mResetOnUnplugHighBatteryLevel;
+ private boolean mResetOnUnplugAfterSignificantCharge;
+ public Builder() {
+ mResetOnUnplugHighBatteryLevel = true;
+ mResetOnUnplugAfterSignificantCharge = true;
+ }
+
+ /**
+ * Build the BatteryStatsConfig.
+ */
+ public BatteryStatsConfig build() {
+ return new BatteryStatsConfig(this);
+ }
+
+ /**
+ * Set whether a BatteryStats reset should occur on unplug when the battery level is
+ * high.
+ */
+ public Builder setResetOnUnplugHighBatteryLevel(boolean reset) {
+ mResetOnUnplugHighBatteryLevel = reset;
+ return this;
+ }
+
+ /**
+ * Set whether a BatteryStats reset should occur on unplug if the battery charge a
+ * significant amount since it has been plugged in.
+ */
+ public Builder setResetOnUnplugAfterSignificantCharge(boolean reset) {
+ mResetOnUnplugAfterSignificantCharge = reset;
+ return this;
+ }
+ }
+
+ }
+
+ /** Handles calls to AlarmManager */
+ public interface AlarmInterface {
+ /** Schedule an RTC alarm */
+ void schedule(long rtcTimeMs, long windowLengthMs);
+ /** Cancel the previously scheduled alarm */
+ void cancel();
+ }
+
private final PlatformIdleStateCallback mPlatformIdleStateCallback;
private final Runnable mDeferSetCharging = new Runnable() {
@@ -717,6 +802,7 @@
protected boolean mHaveBatteryLevel = false;
protected boolean mRecordingHistory = false;
int mNumHistoryItems;
+ private long mBatteryPluggedInRealTimeMs = 0;
private static final int HISTORY_TAG_INDEX_LIMIT = 0x7ffe;
private static final int MAX_HISTORY_TAG_STRING_LENGTH = 1024;
@@ -1459,6 +1545,13 @@
@GuardedBy("this")
protected final Constants mConstants;
+ @VisibleForTesting
+ @GuardedBy("this")
+ protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
+
+ @VisibleForTesting
+ protected AlarmInterface mLongPlugInAlarmInterface = null;
+
/*
* Holds a SamplingTimer associated with each Resource Power Manager state and voter,
* recording their times when on-battery (regardless of screen state).
@@ -1625,12 +1718,13 @@
public BatteryStatsImpl(Clock clock, File historyDirectory) {
init(clock);
mStartClockTimeMs = clock.currentTimeMillis();
- mCheckinFile = null;
mDailyFile = null;
if (historyDirectory == null) {
+ mCheckinFile = null;
mStatsFile = null;
mBatteryStatsHistory = new BatteryStatsHistory(mHistoryBuffer);
} else {
+ mCheckinFile = new AtomicFile(new File(historyDirectory, "batterystats-checkin.bin"));
mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
mBatteryStatsHistory = new BatteryStatsHistory(this, historyDirectory, mHistoryBuffer);
}
@@ -12548,6 +12642,27 @@
}
/**
+ * Injects BatteryStatsConfig
+ */
+ public void setBatteryStatsConfig(BatteryStatsConfig config) {
+ synchronized (this) {
+ mBatteryStatsConfig = config;
+ }
+ }
+
+ /**
+ * Injects a LongPlugInAlarmHandler
+ */
+ public void setLongPlugInAlarmInterface(AlarmInterface longPlugInAlarmInterface) {
+ synchronized (this) {
+ mLongPlugInAlarmInterface = longPlugInAlarmInterface;
+ if (!mOnBattery) {
+ scheduleNextResetWhilePluggedInCheck();
+ }
+ }
+ }
+
+ /**
* Starts tracking CPU time-in-state for threads of the system server process,
* keeping a separate account of threads receiving incoming binder calls.
*/
@@ -13019,12 +13134,12 @@
}
@GuardedBy("this")
- public void resetAllStatsCmdLocked() {
+ public void resetAllStatsAndHistoryLocked(int reason) {
final long mSecUptime = mClock.uptimeMillis();
long uptimeUs = mSecUptime * 1000;
long mSecRealtime = mClock.elapsedRealtime();
long realtimeUs = mSecRealtime * 1000;
- resetAllStatsLocked(mSecUptime, mSecRealtime, RESET_REASON_ADB_COMMAND);
+ resetAllStatsLocked(mSecUptime, mSecRealtime, reason);
mDischargeStartLevel = mHistoryCur.batteryLevel;
pullPendingStateUpdatesLocked();
addHistoryRecordLocked(mSecRealtime, mSecUptime);
@@ -15498,6 +15613,73 @@
}
/**
+ * Might reset battery stats if conditions are met. Assumed the device is currently plugged in.
+ */
+ @GuardedBy("this")
+ public void maybeResetWhilePluggedInLocked() {
+ final long elapsedRealtimeMs = mClock.elapsedRealtime();
+ if (shouldResetWhilePluggedInLocked(elapsedRealtimeMs)) {
+ Slog.i(TAG,
+ "Resetting due to long plug in duration. elapsed time = " + elapsedRealtimeMs
+ + " ms, last plug in time = " + mBatteryPluggedInRealTimeMs
+ + " ms, last reset time = " + mRealtimeStartUs / 1000);
+ resetAllStatsAndHistoryLocked(RESET_REASON_PLUGGED_IN_FOR_LONG_DURATION);
+ }
+
+ scheduleNextResetWhilePluggedInCheck();
+ }
+
+ @GuardedBy("this")
+ private void scheduleNextResetWhilePluggedInCheck() {
+ if (mLongPlugInAlarmInterface != null) {
+ final long timeoutMs = mClock.currentTimeMillis()
+ + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
+ * DateUtils.HOUR_IN_MILLIS;
+ Calendar nextAlarm = Calendar.getInstance();
+ nextAlarm.setTimeInMillis(timeoutMs);
+
+ // Find the 2 AM the same day as the end of the minimum duration.
+ // This logic does not handle a Daylight Savings transition, or a timezone change
+ // while the alarm has been set. The need to reset after a long period while plugged
+ // in is not strict enough to warrant a well architected out solution.
+ nextAlarm.set(Calendar.MILLISECOND, 0);
+ nextAlarm.set(Calendar.SECOND, 0);
+ nextAlarm.set(Calendar.MINUTE, 0);
+ nextAlarm.set(Calendar.HOUR_OF_DAY, 2);
+ long nextTimeMs = nextAlarm.getTimeInMillis();
+ if (nextTimeMs < timeoutMs) {
+ // The 2AM on the day of the timeout, move on the next day.
+ nextTimeMs += DateUtils.DAY_IN_MILLIS;
+ }
+ mLongPlugInAlarmInterface.schedule(nextTimeMs, DateUtils.HOUR_IN_MILLIS);
+ }
+ }
+
+
+ @GuardedBy("this")
+ private boolean shouldResetWhilePluggedInLocked(long elapsedRealtimeMs) {
+ if (mNoAutoReset) return false;
+ if (!mSystemReady) return false;
+
+ final long pluggedInThresholdMs = mBatteryPluggedInRealTimeMs
+ + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
+ * DateUtils.HOUR_IN_MILLIS;
+ if (elapsedRealtimeMs >= pluggedInThresholdMs) {
+ // The device has been plugged in for a long time.
+ final long resetThresholdMs = mRealtimeStartUs / 1000
+ + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS
+ * DateUtils.HOUR_IN_MILLIS;
+ if (elapsedRealtimeMs >= resetThresholdMs) {
+ // And it has been a long time since the last reset.
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
* Notifies BatteryStatsImpl that the system server is ready.
*/
public void onSystemReady() {
@@ -15505,6 +15687,32 @@
}
@GuardedBy("this")
+ private boolean shouldResetOnUnplugLocked(int batteryStatus, int batteryLevel) {
+ if (mNoAutoReset) return false;
+ if (!mSystemReady) return false;
+ if (mBatteryStatsConfig.shouldResetOnUnplugHighBatteryLevel()) {
+ // Allow resetting due to currently being at high battery level
+ if (batteryStatus == BatteryManager.BATTERY_STATUS_FULL) return true;
+ if (batteryLevel >= 90) return true;
+ }
+ if (mBatteryStatsConfig.shouldResetOnUnplugAfterSignificantCharge()) {
+ // Allow resetting after a significant charge (from a very low level to a now very
+ // high level).
+ if (mDischargePlugLevel < 20 && batteryLevel >= 80) return true;
+ }
+ if (getHighDischargeAmountSinceCharge() >= 200) {
+ // Reset the stats if battery got partially charged and discharged repeatedly without
+ // ever reaching the full charge.
+ // This reset is done in order to prevent stats sessions from going on forever.
+ // Exceedingly long battery sessions would lead to an overflow of
+ // data structures such as mWakeupReasonStats.
+ return true;
+ }
+
+ return false;
+ }
+
+ @GuardedBy("this")
protected void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime,
final boolean onBattery, final int oldStatus, final int level, final int chargeUah) {
boolean doWrite = false;
@@ -15516,23 +15724,10 @@
final long realtimeUs = mSecRealtime * 1000;
final int screenState = mScreenState;
if (onBattery) {
- // We will reset our status if we are unplugging after the
- // battery was last full, or the level is at 100, or
- // we have gone through a significant charge (from a very low
- // level to a now very high level).
- // Also, we will reset the stats if battery got partially charged
- // and discharged repeatedly without ever reaching the full charge.
- // This reset is done in order to prevent stats sessions from going on forever.
- // Exceedingly long battery sessions would lead to an overflow of
- // data structures such as mWakeupReasonStats.
boolean reset = false;
- if (!mNoAutoReset && mSystemReady
- && (oldStatus == BatteryManager.BATTERY_STATUS_FULL
- || level >= 90
- || (mDischargeCurrentLevel < 20 && level >= 80)
- || getHighDischargeAmountSinceCharge() >= 200)) {
+ if (shouldResetOnUnplugLocked(oldStatus, level)) {
Slog.i(TAG, "Resetting battery stats: level=" + level + " status=" + oldStatus
- + " dischargeLevel=" + mDischargeCurrentLevel
+ + " dischargeLevel=" + mDischargePlugLevel
+ " lowAmount=" + getLowDischargeAmountSinceCharge()
+ " highAmount=" + getHighDischargeAmountSinceCharge());
// Before we write, collect a snapshot of the final aggregated
@@ -15589,6 +15784,9 @@
mInitStepMode = mCurStepMode;
mModStepMode = 0;
pullPendingStateUpdatesLocked();
+ if (mLongPlugInAlarmInterface != null) {
+ mLongPlugInAlarmInterface.cancel();
+ }
mHistoryCur.batteryLevel = (byte)level;
mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
if (DEBUG_HISTORY) Slog.v(TAG, "Battery unplugged to: "
@@ -15620,6 +15818,7 @@
mLastChargingStateLevel = level;
mOnBattery = mOnBatteryInternal = false;
pullPendingStateUpdatesLocked();
+ mBatteryPluggedInRealTimeMs = mSecRealtime;
mHistoryCur.batteryLevel = (byte)level;
mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
if (DEBUG_HISTORY) Slog.v(TAG, "Battery plugged to: "
@@ -15637,6 +15836,7 @@
mMaxChargeStepLevel = level;
mInitStepMode = mCurStepMode;
mModStepMode = 0;
+ scheduleNextResetWhilePluggedInCheck();
}
if (doWrite || (mLastWriteTimeMs + (60 * 1000)) < mSecRealtime) {
if (mStatsFile != null && mBatteryStatsHistory.getActiveFile() != null) {
@@ -16651,6 +16851,8 @@
public static final String KEY_MAX_HISTORY_BUFFER_KB = "max_history_buffer_kb";
public static final String KEY_BATTERY_CHARGED_DELAY_MS =
"battery_charged_delay_ms";
+ public static final String KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS =
+ "reset_while_plugged_in_minimum_duration_hours";
private static final boolean DEFAULT_TRACK_CPU_ACTIVE_CLUSTER_TIME = true;
private static final long DEFAULT_KERNEL_UID_READERS_THROTTLE_TIME = 1_000;
@@ -16663,6 +16865,8 @@
private static final int DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE = 64;
private static final int DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB = 64; /*Kilo Bytes*/
private static final int DEFAULT_BATTERY_CHARGED_DELAY_MS = 900000; /* 15 min */
+ // Little less than 2 days
+ private static final int DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS = 47;
public boolean TRACK_CPU_ACTIVE_CLUSTER_TIME = DEFAULT_TRACK_CPU_ACTIVE_CLUSTER_TIME;
/* Do not set default value for KERNEL_UID_READERS_THROTTLE_TIME. Need to trigger an
@@ -16678,6 +16882,8 @@
public int MAX_HISTORY_FILES;
public int MAX_HISTORY_BUFFER; /*Bytes*/
public int BATTERY_CHARGED_DELAY_MS = DEFAULT_BATTERY_CHARGED_DELAY_MS;
+ public int RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS =
+ DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS;
private ContentResolver mResolver;
private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -16754,6 +16960,11 @@
DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB
: DEFAULT_MAX_HISTORY_BUFFER_KB)
* 1024;
+
+ RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS = mParser.getInt(
+ KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS,
+ DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS);
+
updateBatteryChargedDelayMsLocked();
}
}
@@ -16808,6 +17019,8 @@
pw.println(MAX_HISTORY_BUFFER/1024);
pw.print(KEY_BATTERY_CHARGED_DELAY_MS); pw.print("=");
pw.println(BATTERY_CHARGED_DELAY_MS);
+ pw.print(KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS); pw.print("=");
+ pw.println(RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS);
}
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index dafa0ad..58c8b29 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6092,4 +6092,9 @@
<!-- Whether to show weather on the lock screen by default. -->
<bool name="config_lockscreenWeatherEnabledByDefault">false</bool>
+
+ <!-- Whether to reset Battery Stats on unplug when the battery level is high. -->
+ <bool name="config_batteryStatsResetOnUnplugHighBatteryLevel">true</bool>
+ <!-- Whether to reset Battery Stats on unplug if the battery was significantly charged -->
+ <bool name="config_batteryStatsResetOnUnplugAfterSignificantCharge">true</bool>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 591ba5f..8addca2 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4903,4 +4903,7 @@
<!-- Whether to show weather on the lockscreen by default. -->
<java-symbol type="bool" name="config_lockscreenWeatherEnabledByDefault" />
+
+ <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" />
+ <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" />
</resources>
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsResetTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsResetTest.java
new file mode 100644
index 0000000..9c2d332
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsResetTest.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2023 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.internal.os;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.os.BatteryManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BatteryStatsResetTest {
+
+ private static final int BATTERY_NOMINAL_VOLTAGE_MV = 3700;
+ private static final int BATTERY_CAPACITY_UAH = 4_000_000;
+ private static final int BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL = 100;
+
+ private MockClock mMockClock;
+ private MockBatteryStatsImpl mBatteryStatsImpl;
+
+
+ /**
+ * Battery status. Must be one of the following:
+ * {@link BatteryManager#BATTERY_STATUS_UNKNOWN}
+ * {@link BatteryManager#BATTERY_STATUS_CHARGING}
+ * {@link BatteryManager#BATTERY_STATUS_DISCHARGING}
+ * {@link BatteryManager#BATTERY_STATUS_NOT_CHARGING}
+ * {@link BatteryManager#BATTERY_STATUS_FULL}
+ */
+ private int mBatteryStatus;
+ /**
+ * Battery health. Must be one of the following:
+ * {@link BatteryManager#BATTERY_HEALTH_UNKNOWN}
+ * {@link BatteryManager#BATTERY_HEALTH_GOOD}
+ * {@link BatteryManager#BATTERY_HEALTH_OVERHEAT}
+ * {@link BatteryManager#BATTERY_HEALTH_DEAD}
+ * {@link BatteryManager#BATTERY_HEALTH_OVER_VOLTAGE}
+ * {@link BatteryManager#BATTERY_HEALTH_UNSPECIFIED_FAILURE}
+ * {@link BatteryManager#BATTERY_HEALTH_COLD}
+ */
+ private int mBatteryHealth;
+ /**
+ * Battery plug type. Can be the union of any number of the following flags:
+ * {@link BatteryManager#BATTERY_PLUGGED_AC}
+ * {@link BatteryManager#BATTERY_PLUGGED_USB}
+ * {@link BatteryManager#BATTERY_PLUGGED_WIRELESS}
+ * {@link BatteryManager#BATTERY_PLUGGED_DOCK}
+ *
+ * Zero means the device is unplugged.
+ */
+ private int mBatteryPlugType;
+ private int mBatteryLevel;
+ private int mBatteryTemp;
+ private int mBatteryVoltageMv;
+ private int mBatteryChargeUah;
+ private int mBatteryChargeFullUah;
+ private long mBatteryChargeTimeToFullSeconds;
+
+ @Before
+ public void setUp() {
+ final Context context = InstrumentationRegistry.getContext();
+
+ mMockClock = new MockClock();
+ mBatteryStatsImpl = new MockBatteryStatsImpl(mMockClock, context.getFilesDir());
+ mBatteryStatsImpl.onSystemReady();
+
+
+ // Set up the battery state. Start off with a fully charged plugged in battery.
+ mBatteryStatus = BatteryManager.BATTERY_STATUS_FULL;
+ mBatteryHealth = BatteryManager.BATTERY_HEALTH_GOOD;
+ mBatteryPlugType = BatteryManager.BATTERY_PLUGGED_USB;
+ mBatteryLevel = 100;
+ mBatteryTemp = 70; // Arbitrary reasonable temperature.
+ mBatteryVoltageMv = BATTERY_NOMINAL_VOLTAGE_MV;
+ mBatteryChargeUah = BATTERY_CAPACITY_UAH;
+ mBatteryChargeFullUah = BATTERY_CAPACITY_UAH;
+ mBatteryChargeTimeToFullSeconds = 0;
+ }
+
+ @Test
+ public void testResetOnUnplug_highBatteryLevel() {
+ mBatteryStatsImpl.setBatteryStatsConfig(
+ new BatteryStatsImpl.BatteryStatsConfig.Builder()
+ .setResetOnUnplugHighBatteryLevel(true)
+ .build());
+
+ long expectedResetTimeUs = 0;
+
+ unplugBattery();
+ dischargeToLevel(60);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(80);
+ unplugBattery();
+ // Reset should not occur until battery level above 90.
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(95);
+ // Reset should not occur until unplug.
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ unplugBattery();
+ // Reset should occur on unplug now that battery level is high (>=90)
+ expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // disable high battery level reset on unplug.
+ mBatteryStatsImpl.setBatteryStatsConfig(
+ new BatteryStatsImpl.BatteryStatsConfig.Builder()
+ .setResetOnUnplugHighBatteryLevel(false)
+ .build());
+
+ dischargeToLevel(60);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(95);
+ unplugBattery();
+ // Reset should not occur since the high battery level logic has been disabled.
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+ }
+
+ @Test
+ public void testResetOnUnplug_significantCharge() {
+ mBatteryStatsImpl.setBatteryStatsConfig(
+ new BatteryStatsImpl.BatteryStatsConfig.Builder()
+ .setResetOnUnplugAfterSignificantCharge(true)
+ .build());
+ long expectedResetTimeUs = 0;
+
+ unplugBattery();
+ // Battery level dropped below 20%.
+ dischargeToLevel(15);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(50);
+ unplugBattery();
+ // Reset should not occur until battery level above 80
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(85);
+ unplugBattery();
+ // Reset should not occur because the charge session did not go from 20% to 80%
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Battery level dropped below 20%.
+ dischargeToLevel(15);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(85);
+ unplugBattery();
+ // Reset should occur after significant charge amount.
+ expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // disable reset on unplug after significant charge.
+ mBatteryStatsImpl.setBatteryStatsConfig(
+ new BatteryStatsImpl.BatteryStatsConfig.Builder()
+ .setResetOnUnplugAfterSignificantCharge(false)
+ .build());
+
+ // Battery level dropped below 20%.
+ dischargeToLevel(15);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(85);
+ unplugBattery();
+ // Reset should not occur after significant charge amount.
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+ }
+
+ @Test
+ public void testResetOnUnplug_manyPartialCharges() {
+ long expectedResetTimeUs = 0;
+
+ unplugBattery();
+ // Cumulative battery discharged: 60%.
+ dischargeToLevel(40);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(80);
+ unplugBattery();
+ // Reset should not occur
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Cumulative battery discharged: 100%.
+ dischargeToLevel(40);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(80);
+ unplugBattery();
+ // Reset should not occur
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Cumulative battery discharged: 140%.
+ dischargeToLevel(40);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(80);
+ unplugBattery();
+ // Reset should not occur
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Cumulative battery discharged: 180%.
+ dischargeToLevel(40);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(80);
+ unplugBattery();
+ // Reset should not occur
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Cumulative battery discharged: 220%.
+ dischargeToLevel(40);
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ chargeToLevel(80);
+ unplugBattery();
+ // Should reset after >200% of cumulative battery discharge
+ expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+ }
+
+ @Test
+ public void testResetWhilePluggedIn_longPlugIn() {
+ // disable high battery level reset on unplug.
+ mBatteryStatsImpl.setBatteryStatsConfig(
+ new BatteryStatsImpl.BatteryStatsConfig.Builder()
+ .setResetOnUnplugHighBatteryLevel(false)
+ .setResetOnUnplugAfterSignificantCharge(false)
+ .build());
+ long expectedResetTimeUs = 0;
+
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+ mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+ // Reset should not occur
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Increment time a day
+ incTimeMs(24L * 60L * 60L * 1000L);
+ mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+ // Reset should still not occur
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Increment time a day
+ incTimeMs(24L * 60L * 60L * 1000L);
+ mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+ // Reset 47 hour threshold crossed, reset should occur.
+ expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Increment time a day
+ incTimeMs(24L * 60L * 60L * 1000L);
+ mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+ // Reset should not occur
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Increment time a day
+ incTimeMs(24L * 60L * 60L * 1000L);
+ mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+ // Reset another 47 hour threshold crossed, reset should occur.
+ expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Increment time a day
+ incTimeMs(24L * 60L * 60L * 1000L);
+ mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+ // Reset should not occur
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ unplugBattery();
+ plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
+
+ // Increment time a day
+ incTimeMs(24L * 60L * 60L * 1000L);
+ mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+ // Reset should not occur, since unplug occurred recently.
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+
+ // Increment time a day
+ incTimeMs(24L * 60L * 60L * 1000L);
+ mBatteryStatsImpl.maybeResetWhilePluggedInLocked();
+ // Reset another 47 hour threshold crossed, reset should occur.
+ expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
+ assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
+ }
+
+ private void dischargeToLevel(int targetLevel) {
+ mBatteryStatus = BatteryManager.BATTERY_STATUS_DISCHARGING;
+ for (int level = mBatteryLevel - 1; level >= targetLevel; level--) {
+ prepareBatteryLevel(level);
+ incTimeMs(5000); // Arbitrary discharge rate.
+ updateBatteryState();
+ }
+ }
+
+ private void chargeToLevel(int targetLevel) {
+ mBatteryStatus = BatteryManager.BATTERY_STATUS_CHARGING;
+ for (int level = mBatteryLevel + 1; level <= targetLevel; level++) {
+ if (level >= 100) mBatteryStatus = BatteryManager.BATTERY_STATUS_FULL;
+ prepareBatteryLevel(level);
+ incTimeMs(BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL * 1000);
+ updateBatteryState();
+ }
+ }
+
+ private void unplugBattery() {
+ mBatteryPlugType = 0;
+ updateBatteryState();
+ }
+
+ private void plugBattery(int type) {
+ mBatteryPlugType |= type;
+ updateBatteryState();
+ }
+
+ private void prepareBatteryLevel(int level) {
+ mBatteryLevel = level;
+ mBatteryChargeUah = mBatteryLevel * mBatteryChargeFullUah / 100;
+ mBatteryChargeTimeToFullSeconds =
+ (100 - mBatteryLevel) * BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL;
+ }
+
+ private void incTimeMs(long milliseconds) {
+ mMockClock.realtime += milliseconds;
+ mMockClock.uptime += milliseconds / 2; // Arbitrary slower uptime accumulation
+ mMockClock.currentTime += milliseconds;
+ }
+
+ private void updateBatteryState() {
+ mBatteryStatsImpl.setBatteryStateLocked(mBatteryStatus, mBatteryHealth, mBatteryPlugType,
+ mBatteryLevel, mBatteryTemp, mBatteryVoltageMv, mBatteryChargeUah,
+ mBatteryChargeFullUah, mBatteryChargeTimeToFullSeconds,
+ mMockClock.elapsedRealtime(), mMockClock.uptimeMillis(),
+ mMockClock.currentTimeMillis());
+ }
+}
+
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
index 2742861..ae2d1af 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
@@ -342,7 +342,7 @@
Context context = InstrumentationRegistry.getContext();
BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
mStatsRule.setCurrentTime(5 * MINUTE_IN_MS);
- batteryStats.resetAllStatsCmdLocked();
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
BatteryUsageStatsStore batteryUsageStatsStore = new BatteryUsageStatsStore(context,
batteryStats, new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"),
@@ -357,14 +357,14 @@
batteryStats.noteFlashlightOffLocked(APP_UID,
20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
mStatsRule.setCurrentTime(25 * MINUTE_IN_MS);
- batteryStats.resetAllStatsCmdLocked();
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
batteryStats.noteFlashlightOnLocked(APP_UID,
30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
batteryStats.noteFlashlightOffLocked(APP_UID,
50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
mStatsRule.setCurrentTime(55 * MINUTE_IN_MS);
- batteryStats.resetAllStatsCmdLocked();
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
// This section should be ignored because the timestamp is out or range
batteryStats.noteFlashlightOnLocked(APP_UID,
@@ -372,7 +372,7 @@
batteryStats.noteFlashlightOffLocked(APP_UID,
70 * MINUTE_IN_MS, 70 * MINUTE_IN_MS);
mStatsRule.setCurrentTime(75 * MINUTE_IN_MS);
- batteryStats.resetAllStatsCmdLocked();
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
// This section should be ignored because it represents the current stats session
batteryStats.noteFlashlightOnLocked(APP_UID,
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java
index c9729fa..11b9047 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsStoreTest.java
@@ -84,7 +84,7 @@
mMockClock.realtime = 1_000_000;
mMockClock.uptime = 1_000_000;
- mBatteryStats.resetAllStatsCmdLocked();
+ mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
assertThat(timestamps).hasLength(1);
@@ -114,7 +114,7 @@
final int numberOfSnapshots =
(int) (MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES / snapshotFileSize);
for (int i = 0; i < numberOfSnapshots + 2; i++) {
- mBatteryStats.resetAllStatsCmdLocked();
+ mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
mMockClock.realtime += 10_000_000;
mMockClock.uptime += 10_000_000;
@@ -141,7 +141,7 @@
mMockClock.currentTime += 10_000_000;
prepareBatteryStats();
- mBatteryStats.resetAllStatsCmdLocked();
+ mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
}
assertThat(getDirectorySize(mStoreDirectory)).isNotEqualTo(0);
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 207c10c..2f95716 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -21,6 +21,7 @@
import static android.os.BatteryStats.POWER_DATA_UNAVAILABLE;
import android.annotation.NonNull;
+import android.app.AlarmManager;
import android.app.StatsManager;
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothActivityEnergyInfo;
@@ -356,6 +357,16 @@
mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
mStats.setPowerProfileLocked(mPowerProfile);
+
+ final boolean resetOnUnplugHighBatteryLevel = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_batteryStatsResetOnUnplugHighBatteryLevel);
+ final boolean resetOnUnplugAfterSignificantCharge = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_batteryStatsResetOnUnplugAfterSignificantCharge);
+ mStats.setBatteryStatsConfig(
+ new BatteryStatsImpl.BatteryStatsConfig.Builder()
+ .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel)
+ .setResetOnUnplugAfterSignificantCharge(resetOnUnplugAfterSignificantCharge)
+ .build());
mStats.startTrackingSystemServerCpuTime();
if (BATTERY_USAGE_STORE_ENABLED) {
@@ -386,6 +397,18 @@
Slog.e(TAG, "Could not register INetworkManagement event observer " + e);
}
+ final AlarmManager am = mContext.getSystemService(AlarmManager.class);
+ mHandler.post(() -> {
+ synchronized (mStats) {
+ mStats.setLongPlugInAlarmInterface(new AlarmInterface(am, () -> {
+ synchronized (mStats) {
+ if (mStats.isOnBattery()) return;
+ mStats.maybeResetWhilePluggedInLocked();
+ }
+ }));
+ }
+ });
+
synchronized (mPowerStatsLock) {
mPowerStatsInternal = LocalServices.getService(PowerStatsInternal.class);
if (mPowerStatsInternal != null) {
@@ -2259,6 +2282,32 @@
}
}
+ final class AlarmInterface implements BatteryStatsImpl.AlarmInterface,
+ AlarmManager.OnAlarmListener {
+ private AlarmManager mAm;
+ private Runnable mOnAlarm;
+
+ AlarmInterface(AlarmManager am, Runnable onAlarm) {
+ mAm = am;
+ mOnAlarm = onAlarm;
+ }
+
+ @Override
+ public void schedule(long rtcTimeMs, long windowLengthMs) {
+ mAm.setWindow(AlarmManager.RTC, rtcTimeMs, windowLengthMs, TAG, this, mHandler);
+ }
+
+ @Override
+ public void cancel() {
+ mAm.cancel(this);
+ }
+
+ @Override
+ public void onAlarm() {
+ mOnAlarm.run();
+ }
+ }
+
private static native int nativeWaitWakeup(ByteBuffer outBuffer);
private void dumpHelp(PrintWriter pw) {
@@ -2445,7 +2494,8 @@
} else if ("--reset-all".equals(arg)) {
awaitCompletion();
synchronized (mStats) {
- mStats.resetAllStatsCmdLocked();
+ mStats.resetAllStatsAndHistoryLocked(
+ BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
mBatteryUsageStatsStore.removeAllSnapshots();
pw.println("Battery stats and history reset.");
noOutput = true;
@@ -2453,7 +2503,8 @@
} else if ("--reset".equals(arg)) {
awaitCompletion();
synchronized (mStats) {
- mStats.resetAllStatsCmdLocked();
+ mStats.resetAllStatsAndHistoryLocked(
+ BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
pw.println("Battery stats reset.");
noOutput = true;
}