Add config to control BatteryStats reset logic
Also, fix the significant charge reset logic and add BatteryStats reset
tests.
Fix: 269538224
Bug: 256919205
Test: atest BatteryStatsResetTest
Change-Id: Id4a6dd22708ddee38dd6c10ceb6c81f6a7bfb44f
Merged-In: Id4a6dd22708ddee38dd6c10ceb6c81f6a7bfb44f
(cherry picked from commit ebd856f4d74ca5aed7f0b943c387c84355219b98)
Merged-In: Id4a6dd22708ddee38dd6c10ceb6c81f6a7bfb44f
(cherry picked from commit 68ccdde00fc790a8f9888e383f416700755ed47d)
Merged-In: Id4a6dd22708ddee38dd6c10ceb6c81f6a7bfb44f
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 6fed26c..4953754 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -387,6 +387,81 @@
}
}
+ /** 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;
+ }
+ }
+
+ }
+
private final PlatformIdleStateCallback mPlatformIdleStateCallback;
private final Runnable mDeferSetCharging = new Runnable() {
@@ -1459,6 +1534,10 @@
@GuardedBy("this")
protected final Constants mConstants;
+ @VisibleForTesting
+ @GuardedBy("this")
+ protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
+
/*
* Holds a SamplingTimer associated with each Resource Power Manager state and voter,
* recording their times when on-battery (regardless of screen state).
@@ -1625,12 +1704,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 +12628,15 @@
}
/**
+ * Injects BatteryStatsConfig
+ */
+ public void setBatteryStatsConfig(BatteryStatsConfig config) {
+ synchronized (this) {
+ mBatteryStatsConfig = config;
+ }
+ }
+
+ /**
* Starts tracking CPU time-in-state for threads of the system server process,
* keeping a separate account of threads receiving incoming binder calls.
*/
@@ -15505,6 +15594,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 +15631,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
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 87ece55..416b7c3 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6083,4 +6083,9 @@
<!-- Whether the lock screen is allowed to run its own live wallpaper,
different from the home screen wallpaper. -->
<bool name="config_independentLockscreenLiveWallpaper">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 15c40c0..ccf6e9a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4892,4 +4892,7 @@
<java-symbol type="id" name="language_picker_header" />
<java-symbol type="dimen" name="status_bar_height_default" />
+
+ <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..cfe8d68
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsResetTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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);
+ }
+
+ 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/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 207c10c..c971d92 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -356,6 +356,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) {