diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 05c6842784278..4bb9c3bafcd3c 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -387,6 +387,81 @@ public class BatteryStatsImpl extends BatteryStats { } } + /** 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() { @@ -1481,6 +1556,10 @@ public class BatteryStatsImpl extends BatteryStats { @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). @@ -1647,12 +1726,13 @@ public class BatteryStatsImpl extends BatteryStats { 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); } @@ -12581,6 +12661,15 @@ public class BatteryStatsImpl extends BatteryStats { return mPowerProfile; } + /** + * 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. @@ -15538,6 +15627,32 @@ public class BatteryStatsImpl extends BatteryStats { mSystemReady = true; } + @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) { @@ -15550,23 +15665,10 @@ public class BatteryStatsImpl extends BatteryStats { 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 1cc82c6155445..4d66feb1f2dc5 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6046,4 +6046,9 @@ false + + + true + + true diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index d98dd9f368cdb..8cd55cd1298ba 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4928,4 +4928,7 @@ + + + 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 0000000000000..cfe8d68b14167 --- /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 8439a7d9b7434..43f8452a47201 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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) {