Merge changes from topic "cherrypick-config_batteryStatsResetOnUnplugHighBatteryLevel-v9xw8cq20i" into udc-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/res/res/values/config.xml b/core/res/res/values/config.xml
index 31b9f9a..5d78927 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6322,4 +6322,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 312ad1f..9640033 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4960,4 +4960,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/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 19235c9..1607566 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -29,6 +29,7 @@
 import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
 import android.annotation.RequiresNoPermission;
+import android.app.AlarmManager;
 import android.app.StatsManager;
 import android.app.usage.NetworkStatsManager;
 import android.bluetooth.BluetoothActivityEnergyInfo;
@@ -370,6 +371,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) {
@@ -401,6 +412,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) {
@@ -2484,6 +2507,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) {
@@ -2684,7 +2733,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;
@@ -2692,7 +2742,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;
                     }
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index add4a89..3d3c5e2 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -82,6 +82,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;
@@ -212,6 +213,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_ENERGY_CONSUMER_BUCKETS_CHANGE = 4;
+    public static final int RESET_REASON_PLUGGED_IN_FOR_LONG_DURATION = 5;
 
     protected Clock mClock;
 
@@ -423,6 +425,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() {
@@ -773,6 +858,7 @@
 
     private boolean mHaveBatteryLevel = false;
     private boolean mBatteryPluggedIn;
+    private long mBatteryPluggedInRealTimeMs = 0;
     private int mBatteryStatus;
     private int mBatteryLevel;
     private int mBatteryPlugType;
@@ -1479,6 +1565,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).
@@ -1646,12 +1739,13 @@
         mHandler = null;
         mConstants = new Constants(mHandler);
         mStartClockTimeMs = clock.currentTimeMillis();
-        mCheckinFile = null;
         mDailyFile = null;
         if (historyDirectory == null) {
+            mCheckinFile = null;
             mStatsFile = null;
             mHistory = new BatteryStatsHistory(mStepDetailsCalculator, mClock);
         } else {
+            mCheckinFile = new AtomicFile(new File(historyDirectory, "batterystats-checkin.bin"));
             mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
             mHistory = new BatteryStatsHistory(historyDirectory, mConstants.MAX_HISTORY_FILES,
                     mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock);
@@ -10954,6 +11048,27 @@
     }
 
     /**
+     * Injects BatteryStatsConfig
+     */
+    public void setBatteryStatsConfig(BatteryStatsConfig config) {
+        synchronized (this) {
+            mBatteryStatsConfig = config;
+        }
+    }
+
+    /**
+     * Injects an AlarmInterface for the long plug in alarm.
+     */
+    public void setLongPlugInAlarmInterface(AlarmInterface longPlugInAlarmInterface) {
+        synchronized (this) {
+            mLongPlugInAlarmInterface = longPlugInAlarmInterface;
+            if (mBatteryPluggedIn) {
+                scheduleNextResetWhilePluggedInCheck();
+            }
+        }
+    }
+
+    /**
      * Starts tracking CPU time-in-state for threads of the system server process,
      * keeping a separate account of threads receiving incoming binder calls.
      */
@@ -11386,12 +11501,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);
         pullPendingStateUpdatesLocked();
         mHistory.writeHistoryItem(mSecRealtime, mSecUptime);
         mDischargeCurrentLevel = mDischargeUnplugLevel = mDischargePlugLevel = mBatteryLevel;
@@ -14051,6 +14166,100 @@
         mRecordAllHistory = true;
     }
 
+    /**
+     * 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;
+        if (!mHistory.isResetEnabled()) 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;
+    }
+
+    @GuardedBy("this")
+    private boolean shouldResetOnUnplugLocked(int batteryStatus, int batteryLevel) {
+        if (mNoAutoReset) return false;
+        if (!mSystemReady) return false;
+        if (!mHistory.isResetEnabled()) 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) {
@@ -14063,24 +14272,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)
-                    && mHistory.isResetEnabled()) {
+            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
@@ -14140,6 +14335,9 @@
                 initActiveHistoryEventsLocked(mSecRealtime, mSecUptime);
             }
             mBatteryPluggedIn = false;
+            if (mLongPlugInAlarmInterface != null) {
+                mLongPlugInAlarmInterface.cancel();
+            }
             mHistory.recordBatteryState(mSecRealtime, mSecUptime, level, mBatteryPluggedIn);
             mDischargeCurrentLevel = mDischargeUnplugLevel = level;
             if (Display.isOnState(screenState)) {
@@ -14163,6 +14361,7 @@
             mOnBattery = mOnBatteryInternal = false;
             pullPendingStateUpdatesLocked();
             mBatteryPluggedIn = true;
+            mBatteryPluggedInRealTimeMs = mSecRealtime;
             mHistory.recordBatteryState(mSecRealtime, mSecUptime, level, mBatteryPluggedIn);
             mDischargeCurrentLevel = mDischargePlugLevel = level;
             if (level < mDischargeUnplugLevel) {
@@ -14176,6 +14375,7 @@
             mMaxChargeStepLevel = level;
             mInitStepMode = mCurStepMode;
             mModStepMode = 0;
+            scheduleNextResetWhilePluggedInCheck();
         }
         if (doWrite || (mLastWriteTimeMs + (60 * 1000)) < mSecRealtime) {
             if (mStatsFile != null && !mHistory.isReadOnly()) {
@@ -15082,6 +15282,8 @@
                 "per_uid_modem_power_model";
         public static final String KEY_PHONE_ON_EXTERNAL_STATS_COLLECTION =
                 "phone_on_external_stats_collection";
+        public static final String KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS =
+                "reset_while_plugged_in_minimum_duration_hours";
 
         public static final String PER_UID_MODEM_POWER_MODEL_MOBILE_RADIO_ACTIVE_TIME_NAME =
                 "mobile_radio_active_time";
@@ -15131,6 +15333,8 @@
         private static final int DEFAULT_PER_UID_MODEM_MODEL =
                 PER_UID_MODEM_POWER_MODEL_MODEM_ACTIVITY_INFO_RX_TX;
         private static final boolean DEFAULT_PHONE_ON_EXTERNAL_STATS_COLLECTION = true;
+        // 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
@@ -15150,6 +15354,8 @@
         public int PER_UID_MODEM_MODEL = DEFAULT_PER_UID_MODEM_MODEL;
         public boolean PHONE_ON_EXTERNAL_STATS_COLLECTION =
                 DEFAULT_PHONE_ON_EXTERNAL_STATS_COLLECTION;
+        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(',');
@@ -15235,6 +15441,10 @@
                         KEY_PHONE_ON_EXTERNAL_STATS_COLLECTION,
                         DEFAULT_PHONE_ON_EXTERNAL_STATS_COLLECTION);
 
+                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();
 
                 onChange();
@@ -15306,6 +15516,8 @@
             pw.println(getPerUidModemModelName(PER_UID_MODEM_MODEL));
             pw.print(KEY_PHONE_ON_EXTERNAL_STATS_COLLECTION); pw.print("=");
             pw.println(PHONE_ON_EXTERNAL_STATS_COLLECTION);
+            pw.print(KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS); pw.print("=");
+            pw.println(RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS);
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsResetTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsResetTest.java
new file mode 100644
index 0000000..a0fb631
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsResetTest.java
@@ -0,0 +1,360 @@
+/*
+ * 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.server.power.stats;
+
+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/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 2f64506..5df0acb 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -371,7 +371,7 @@
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
         mStatsRule.setCurrentTime(5 * MINUTE_IN_MS);
         synchronized (batteryStats) {
-            batteryStats.resetAllStatsCmdLocked();
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
         BatteryUsageStatsStore batteryUsageStatsStore = new BatteryUsageStatsStore(context,
                 batteryStats, new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"),
@@ -391,7 +391,7 @@
         }
         mStatsRule.setCurrentTime(25 * MINUTE_IN_MS);
         synchronized (batteryStats) {
-            batteryStats.resetAllStatsCmdLocked();
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
 
         synchronized (batteryStats) {
@@ -404,7 +404,7 @@
         }
         mStatsRule.setCurrentTime(55 * MINUTE_IN_MS);
         synchronized (batteryStats) {
-            batteryStats.resetAllStatsCmdLocked();
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
 
         // This section should be ignored because the timestamp is out or range
@@ -418,7 +418,7 @@
         }
         mStatsRule.setCurrentTime(75 * MINUTE_IN_MS);
         synchronized (batteryStats) {
-            batteryStats.resetAllStatsCmdLocked();
+            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
 
         // This section should be ignored because it represents the current stats session
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
index 970020f..b846e3a 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
@@ -86,7 +86,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);
@@ -116,7 +116,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;
@@ -143,7 +143,7 @@
             mMockClock.currentTime += 10_000_000;
             prepareBatteryStats();
 
-            mBatteryStats.resetAllStatsCmdLocked();
+            mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
 
         assertThat(getDirectorySize(mStoreDirectory)).isNotEqualTo(0);