Merge "CarService detects recurring overuse" into sc-v2-dev
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index ae487da..7e49fb1 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -167,6 +167,14 @@
          to signal the potential for flash wear. -->
     <integer name="maxExcessiveIoSamplesInWindow">11</integer>
 
+    <!-- The number of days past until the current day considered by car watchdog to
+         attribute recurring overuse to a package.  -->
+    <integer name="recurringResourceOverusePeriodInDays">14</integer>
+
+    <!-- The number of overuses a package has to exceed for car watchdog to attribute
+         recurring overuse.  -->
+    <integer name="recurringResourceOveruseTimes">2</integer>
+
     <!-- The name of an intent to be notified by CarService whenever it detects too many
          samples with excessive I/O activity. Value must either be an empty string, which
          means that no notification will take place, or be in the format of a flattened
diff --git a/service/src/com/android/car/watchdog/CarWatchdogService.java b/service/src/com/android/car/watchdog/CarWatchdogService.java
index f59644a..ae9897d 100644
--- a/service/src/com/android/car/watchdog/CarWatchdogService.java
+++ b/service/src/com/android/car/watchdog/CarWatchdogService.java
@@ -466,11 +466,6 @@
     }
 
     @VisibleForTesting
-    void setRecurringOveruseThreshold(int threshold) {
-        mWatchdogPerfHandler.setRecurringOveruseThreshold(threshold);
-    }
-
-    @VisibleForTesting
     void setUidIoUsageSummaryTopCount(int uidIoUsageSummaryTopCount) {
         mWatchdogPerfHandler.setUidIoUsageSummaryTopCount(uidIoUsageSummaryTopCount);
     }
diff --git a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
index 31b790c..d39ec88 100644
--- a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+++ b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
@@ -115,6 +115,7 @@
 import com.android.car.CarServiceUtils;
 import com.android.car.CarStatsLog;
 import com.android.car.CarUxRestrictionsManagerService;
+import com.android.car.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ConcurrentUtils;
@@ -177,11 +178,6 @@
                     .setTimeoutMillis(10_000)
                     .build();
 
-    // TODO(b/195425666): Define this constant as a resource overlay config with two values:
-    //  1. Recurring overuse period - Period to calculate the recurring overuse.
-    //  2. Recurring overuse threshold - Total overuses for recurring behavior.
-    private static final int RECURRING_OVERUSE_THRESHOLD = 2;
-
     /**
      * Don't distract the user by sending user notifications/dialogs, killing foreground
      * applications, repeatedly killing persistent background services, or disabling any
@@ -212,6 +208,8 @@
     private final WatchdogStorage mWatchdogStorage;
     private final OveruseConfigurationCache mOveruseConfigurationCache;
     private final UserNotificationHelper mUserNotificationHelper;
+    private final int mRecurringOverusePeriodInDays;
+    private final int mRecurringOveruseTimes;
     private final Object mLock = new Object();
     @GuardedBy("mLock")
     private final ArrayMap<String, PackageResourceUsage> mUsageByUserPackage = new ArrayMap<>();
@@ -298,8 +296,11 @@
         mOveruseHandlingDelayMills = OVERUSE_HANDLING_DELAY_MILLS;
         mCurrentUxState = UX_STATE_NO_DISTRACTION;
         mCurrentGarageMode = GarageMode.GARAGE_MODE_OFF;
-        mRecurringOveruseThreshold = RECURRING_OVERUSE_THRESHOLD;
         mUidIoUsageSummaryTopCount = UID_IO_USAGE_SUMMARY_TOP_COUNT;
+        mRecurringOverusePeriodInDays = mContext.getResources().getInteger(
+                R.integer.recurringResourceOverusePeriodInDays);
+        mRecurringOveruseTimes = mContext.getResources().getInteger(
+                R.integer.recurringResourceOveruseTimes);
     }
 
     /** Initializes the handler. */
@@ -307,10 +308,6 @@
         /* First database read is expensive, so post it on a separate handler thread. */
         mServiceHandler.post(() -> {
             readFromDatabase();
-            synchronized (mLock) {
-                checkAndHandleDateChangeLocked();
-                mIsWrittenToDatabase = false;
-            }
             // Set atom pull callbacks only after the internal datastructures are updated. When the
             // pull happens, the service is already initialized and ready to populate the pulled
             // atoms.
@@ -773,8 +770,8 @@
         }
         SparseArray<String> genericPackageNamesByUid = mPackageInfoHandler.getNamesForUids(uids);
         ArraySet<String> overusingUserPackageKeys = new ArraySet<>();
+        checkAndHandleDateChange();
         synchronized (mLock) {
-            checkAndHandleDateChangeLocked();
             for (int i = 0; i < packageIoOveruseStats.size(); ++i) {
                 PackageIoOveruseStats stats = packageIoOveruseStats.get(i);
                 String genericPackageName = genericPackageNamesByUid.get(stats.uid);
@@ -802,10 +799,11 @@
                 if (killableState == KILLABLE_STATE_NEVER) {
                     continue;
                 }
-                if (isRecurringOveruseLocked(usage)) {
+                if (usage.ioUsage.getNotForgivenOveruses() > mRecurringOveruseTimes) {
                     String id = usage.getUniqueId();
                     mActionableUserPackages.add(id);
                     mUserNotifiablePackages.add(id);
+                    usage.ioUsage.forgiveOveruses();
                 }
             }
             if ((mCurrentUxState != UX_STATE_NO_DISTRACTION && !mUserNotifiablePackages.isEmpty())
@@ -819,6 +817,7 @@
                         performOveruseHandlingLocked();
                     }}, mOveruseHandlingDelayMills);
             }
+            mIsWrittenToDatabase = false;
         }
         if (!overusingUserPackageKeys.isEmpty()) {
             pushIoOveruseMetrics(overusingUserPackageKeys);
@@ -985,13 +984,6 @@
         }
     }
 
-    /** Sets the threshold for recurring overuse behavior. */
-    public void setRecurringOveruseThreshold(int threshold) {
-        synchronized (mLock) {
-            mRecurringOveruseThreshold = threshold;
-        }
-    }
-
     /** Sets top N UID I/O usage summaries to report on stats pull. */
     public void setUidIoUsageSummaryTopCount(int uidIoUsageSummaryTopCount) {
         synchronized (mLock) {
@@ -1105,12 +1097,28 @@
                 usage.ioUsage.overwrite(entry.ioUsage);
                 mUsageByUserPackage.put(key, usage);
             }
-            if (!ioStatsEntries.isEmpty()) {
-                /* When mLatestStatsReportDate is null, the latest stats push from daemon hasn't
-                 * happened yet. Thus the cached stats contains only the stats read from database.
-                 */
-                mIsWrittenToDatabase = mLatestStatsReportDate == null;
-                mLatestStatsReportDate = curReportDate;
+            mLatestStatsReportDate = curReportDate;
+        }
+        syncHistoricalNotForgivenOveruses();
+    }
+
+    /** Fetches all historical not forgiven overuses and syncs them with package I/O usages. */
+    private void syncHistoricalNotForgivenOveruses() {
+        List<WatchdogStorage.NotForgivenOverusesEntry> notForgivenOverusesEntries =
+                mWatchdogStorage.getNotForgivenHistoricalIoOveruses(mRecurringOverusePeriodInDays);
+        Slogf.i(TAG, "Read %d not forgiven overuse stats from database",
+                notForgivenOverusesEntries.size());
+        synchronized (mLock) {
+            for (int i = 0; i < notForgivenOverusesEntries.size(); i++) {
+                WatchdogStorage.NotForgivenOverusesEntry entry = notForgivenOverusesEntries.get(i);
+                String key = getUserPackageUniqueId(entry.userId, entry.packageName);
+                PackageResourceUsage usage = mUsageByUserPackage.get(key);
+                if (usage == null) {
+                    usage = new PackageResourceUsage(entry.userId, entry.packageName,
+                            getDefaultKillableStateLocked(entry.packageName));
+                }
+                usage.ioUsage.setHistoricalNotForgivenOveruses(entry.notForgivenOveruses);
+                mUsageByUserPackage.put(key, usage);
             }
         }
     }
@@ -1151,20 +1159,39 @@
 
     @GuardedBy("mLock")
     private void writeStatsLocked() {
-        List<WatchdogStorage.IoUsageStatsEntry> entries =
+        List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries =
                 new ArrayList<>(mUsageByUserPackage.size());
+        SparseArray<List<String>> forgivePackagesByUserId = new SparseArray<>();
         for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
             PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
             if (!usage.ioUsage.hasUsage()) {
                 continue;
             }
-            entries.add(new WatchdogStorage.IoUsageStatsEntry(
-                    usage.userId, usage.genericPackageName, usage.ioUsage));
+            if (usage.ioUsage.shouldForgiveHistoricalOveruses()) {
+                List<String> packagesToForgive = forgivePackagesByUserId.get(usage.userId);
+                if (packagesToForgive == null) {
+                    packagesToForgive = new ArrayList<>();
+                }
+                packagesToForgive.add(usage.genericPackageName);
+                forgivePackagesByUserId.put(usage.userId, packagesToForgive);
+            }
+            ioUsageStatsEntries.add(new WatchdogStorage.IoUsageStatsEntry(usage.userId,
+                    usage.genericPackageName, usage.ioUsage));
         }
-        if (!mWatchdogStorage.saveIoUsageStats(entries)) {
-            Slogf.e(TAG, "Failed to write %d I/O overuse stats to database", entries.size());
+        // Forgive historical overuses before writing the latest stats to disk to avoid forgiving
+        // the latest stats when the write is triggered after date change.
+        if (forgivePackagesByUserId.size() != 0) {
+            mWatchdogStorage.forgiveHistoricalOveruses(forgivePackagesByUserId,
+                    mRecurringOverusePeriodInDays);
+            Slogf.e(TAG, "Attempted to forgive historical overuses for %d users.",
+                    forgivePackagesByUserId.size());
+        }
+        if (!mWatchdogStorage.saveIoUsageStats(ioUsageStatsEntries)) {
+            Slogf.e(TAG, "Failed to write %d I/O overuse stats to database",
+                    ioUsageStatsEntries.size());
         } else {
-            Slogf.i(TAG, "Successfully saved %d I/O overuse stats to database", entries.size());
+            Slogf.i(TAG, "Successfully saved %d I/O overuse stats to database",
+                    ioUsageStatsEntries.size());
         }
     }
 
@@ -1221,23 +1248,24 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private void checkAndHandleDateChangeLocked() {
-        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
-        if (currentDate.equals(mLatestStatsReportDate)) {
-            return;
+    private void checkAndHandleDateChange() {
+        synchronized (mLock) {
+            ZonedDateTime currentDate = mTimeSource.getCurrentDate();
+            if (currentDate.equals(mLatestStatsReportDate)) {
+                return;
+            }
+            // After the first database read or on the first stats sync from the daemon, whichever
+            // happens first, the cached stats would either be empty or initialized from the
+            // database. In either case, don't write to database.
+            if (mLatestStatsReportDate != null && !mIsWrittenToDatabase) {
+                writeStatsLocked();
+            }
+            for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+                mUsageByUserPackage.valueAt(i).resetStats();
+            }
+            mLatestStatsReportDate = currentDate;
         }
-        /* After the first database read or on the first stats sync from the daemon, whichever
-         * happens first, the cached stats would either be empty or initialized from the database.
-         * In either case, don't write to database.
-         */
-        if (mLatestStatsReportDate != null && !mIsWrittenToDatabase) {
-            writeStatsLocked();
-        }
-        for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
-            mUsageByUserPackage.valueAt(i).resetStats();
-        }
-        mLatestStatsReportDate = currentDate;
+        syncHistoricalNotForgivenOveruses();
         if (DEBUG) {
             Slogf.d(TAG, "Handled date change successfully");
         }
@@ -1259,14 +1287,6 @@
         return usage;
     }
 
-    @GuardedBy("mLock")
-    private boolean isRecurringOveruseLocked(PackageResourceUsage usage) {
-        // TODO(b/195425666): Look up I/O overuse history and determine whether or not the package
-        //  has recurring I/O overuse behavior.
-        return usage.ioUsage.getInternalIoOveruseStats().totalOveruses
-                > mRecurringOveruseThreshold;
-    }
-
     private IoOveruseStats getIoOveruseStatsForPeriod(int userId, String genericPackageName,
             @CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
         synchronized (mLock) {
@@ -2437,35 +2457,84 @@
     public static final class PackageIoUsage {
         private static final android.automotive.watchdog.PerStateBytes DEFAULT_PER_STATE_BYTES =
                 new android.automotive.watchdog.PerStateBytes();
+        private static final int MISSING_VALUE = -1;
+
         private android.automotive.watchdog.IoOveruseStats mIoOveruseStats;
         private android.automotive.watchdog.PerStateBytes mForgivenWriteBytes;
+        private int mForgivenOveruses;
+        private int mHistoricalNotForgivenOveruses;
         private int mTotalTimesKilled;
 
         private PackageIoUsage() {
             mForgivenWriteBytes = DEFAULT_PER_STATE_BYTES;
+            mForgivenOveruses = 0;
+            mHistoricalNotForgivenOveruses = MISSING_VALUE;
             mTotalTimesKilled = 0;
         }
 
         public PackageIoUsage(android.automotive.watchdog.IoOveruseStats ioOveruseStats,
-                android.automotive.watchdog.PerStateBytes forgivenWriteBytes,
+                android.automotive.watchdog.PerStateBytes forgivenWriteBytes, int forgivenOveruses,
                 int totalTimesKilled) {
             mIoOveruseStats = ioOveruseStats;
             mForgivenWriteBytes = forgivenWriteBytes;
+            mForgivenOveruses = forgivenOveruses;
             mTotalTimesKilled = totalTimesKilled;
+            mHistoricalNotForgivenOveruses = MISSING_VALUE;
         }
 
+        /** Returns the I/O overuse stats related to the package. */
         public android.automotive.watchdog.IoOveruseStats getInternalIoOveruseStats() {
             return mIoOveruseStats;
         }
 
+        /** Returns the forgiven write bytes. */
         public android.automotive.watchdog.PerStateBytes getForgivenWriteBytes() {
             return mForgivenWriteBytes;
         }
 
+        /** Returns the number of forgiven overuses today. */
+        public int getForgivenOveruses() {
+            return mForgivenOveruses;
+        }
+
+        /**
+         * Returns the number of not forgiven overuses. These are overuses that have not been
+         * attributed previously to a package's recurring overuse.
+         */
+        public int getNotForgivenOveruses() {
+            if (!hasUsage()) {
+                return 0;
+            }
+            int historicalNotForgivenOveruses =
+                    mHistoricalNotForgivenOveruses != MISSING_VALUE
+                            ? mHistoricalNotForgivenOveruses : 0;
+            return (mIoOveruseStats.totalOveruses - mForgivenOveruses)
+                    + historicalNotForgivenOveruses;
+        }
+
+        /** Sets historical not forgiven overuses. */
+        public void setHistoricalNotForgivenOveruses(int historicalNotForgivenOveruses) {
+            mHistoricalNotForgivenOveruses = historicalNotForgivenOveruses;
+        }
+
+        /** Forgives all the I/O overuse stats' overuses. */
+        public void forgiveOveruses() {
+            if (!hasUsage()) {
+                return;
+            }
+            mForgivenOveruses = mIoOveruseStats.totalOveruses;
+            mHistoricalNotForgivenOveruses = 0;
+        }
+
+        /** Returns the total number of times the package was killed. */
         public int getTotalTimesKilled() {
             return mTotalTimesKilled;
         }
 
+        boolean shouldForgiveHistoricalOveruses() {
+            return mHistoricalNotForgivenOveruses != MISSING_VALUE;
+        }
+
         boolean hasUsage() {
             return mIoOveruseStats != null;
         }
@@ -2474,6 +2543,7 @@
             mIoOveruseStats = ioUsage.mIoOveruseStats;
             mForgivenWriteBytes = ioUsage.mForgivenWriteBytes;
             mTotalTimesKilled = ioUsage.mTotalTimesKilled;
+            mHistoricalNotForgivenOveruses = ioUsage.mHistoricalNotForgivenOveruses;
         }
 
         void update(android.automotive.watchdog.IoOveruseStats internalStats,
@@ -2503,6 +2573,8 @@
         void resetStats() {
             mIoOveruseStats = null;
             mForgivenWriteBytes = DEFAULT_PER_STATE_BYTES;
+            mForgivenOveruses = 0;
+            mHistoricalNotForgivenOveruses = MISSING_VALUE;
             mTotalTimesKilled = 0;
         }
     }
diff --git a/service/src/com/android/car/watchdog/WatchdogStorage.java b/service/src/com/android/car/watchdog/WatchdogStorage.java
index 27ead48..57baf06 100644
--- a/service/src/com/android/car/watchdog/WatchdogStorage.java
+++ b/service/src/com/android/car/watchdog/WatchdogStorage.java
@@ -34,6 +34,7 @@
 import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.Slog;
+import android.util.SparseArray;
 
 import com.android.car.CarLog;
 import com.android.internal.annotations.GuardedBy;
@@ -262,6 +263,71 @@
     }
 
     /**
+     * Returns the aggregated historical overuses minus the forgiven overuses for all saved
+     * packages. Forgiven overuses are overuses that have been attributed previously to a package's
+     * recurring overuse.
+     */
+    public List<NotForgivenOverusesEntry> getNotForgivenHistoricalIoOveruses(int numDaysAgo) {
+        ZonedDateTime currentDate =
+                mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+        long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond();
+        long excludingEndEpochSeconds = currentDate.toEpochSecond();
+        ArrayMap<String, Integer> notForgivenOverusesById;
+        try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+            notForgivenOverusesById = IoUsageStatsTable.queryNotForgivenHistoricalOveruses(db,
+                    includingStartEpochSeconds, excludingEndEpochSeconds);
+        }
+        List<NotForgivenOverusesEntry> notForgivenOverusesEntries = new ArrayList<>();
+        for (int i = 0; i < notForgivenOverusesById.size(); i++) {
+            String id = notForgivenOverusesById.keyAt(i);
+            UserPackage userPackage = mUserPackagesById.get(id);
+            if (userPackage == null) {
+                Slogf.w(TAG,
+                        "Failed to find user id and package name for unique database id: '%s'",
+                        id);
+                continue;
+            }
+            notForgivenOverusesEntries.add(new NotForgivenOverusesEntry(userPackage.getUserId(),
+                    userPackage.getPackageName(), notForgivenOverusesById.valueAt(i)));
+        }
+        return notForgivenOverusesEntries;
+    }
+
+    /**
+     * Forgives all historical overuses between yesterday and {@code numDaysAgo}
+     * for a list of specific {@code userIds} and {@code packageNames}.
+     */
+    public void forgiveHistoricalOveruses(SparseArray<List<String>> packagesByUserId,
+            int numDaysAgo) {
+        if (packagesByUserId.size() == 0) {
+            Slogf.w(TAG, "No I/O usage stats provided to forgive historical overuses.");
+            return;
+        }
+        ZonedDateTime currentDate =
+                mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+        long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond();
+        long excludingEndEpochSeconds = currentDate.toEpochSecond();
+        List<String> uniqueIds = new ArrayList<>();
+        for (int i = 0; i < packagesByUserId.size(); i++) {
+            int userId = packagesByUserId.keyAt(i);
+            List<String> packages = packagesByUserId.valueAt(i);
+            for (int pkgIdx = 0; pkgIdx < packages.size(); pkgIdx++) {
+                UserPackage userPackage =
+                        mUserPackagesByKey.get(UserPackage.getKey(userId, packages.get(pkgIdx)));
+                if (userPackage == null) {
+                    // Packages without historical stats don't have userPackage entry.
+                    continue;
+                }
+                uniqueIds.add(userPackage.getUniqueId());
+            }
+        }
+        try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+            IoUsageStatsTable.forgiveHistoricalOverusesForPackage(db, uniqueIds,
+                    includingStartEpochSeconds, excludingEndEpochSeconds);
+        }
+    }
+
+    /**
      * Deletes all user package settings and resource stats for all non-alive users.
      *
      * @param aliveUserIds Array of alive user ids.
@@ -572,6 +638,46 @@
         }
     }
 
+    /** Defines the not forgiven overuses stored in the IoUsageStatsTable. */
+    static final class NotForgivenOverusesEntry {
+        public final @UserIdInt int userId;
+        public final String packageName;
+        public final int notForgivenOveruses;
+
+        NotForgivenOverusesEntry(@UserIdInt int userId,
+                String packageName, int notForgivenOveruses) {
+            this.userId = userId;
+            this.packageName = packageName;
+            this.notForgivenOveruses = notForgivenOveruses;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof NotForgivenOverusesEntry)) {
+                return false;
+            }
+            NotForgivenOverusesEntry other = (NotForgivenOverusesEntry) obj;
+            return userId == other.userId
+                    && packageName.equals(other.packageName)
+                    && notForgivenOveruses == other.notForgivenOveruses;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(userId, packageName, notForgivenOveruses);
+        }
+
+        @Override
+        public String toString() {
+            return "NotForgivenOverusesEntry {UserId: " + userId
+                    + ", Package name: " + packageName
+                    + ", Not forgiven overuses: " + notForgivenOveruses + "}";
+        }
+    }
+
     /**
      * Defines the contents and queries for the I/O usage stats table.
      */
@@ -634,8 +740,7 @@
             values.put(COLUMN_USER_PACKAGE_ID, userPackageId);
             values.put(COLUMN_DATE_EPOCH, statsDateEpochSeconds);
             values.put(COLUMN_NUM_OVERUSES, ioOveruseStats.totalOveruses);
-            /* TODO(b/195425666): Put total forgiven overuses for the day. */
-            values.put(COLUMN_NUM_FORGIVEN_OVERUSES, 0);
+            values.put(COLUMN_NUM_FORGIVEN_OVERUSES, entry.ioUsage.getForgivenOveruses());
             values.put(COLUMN_NUM_TIMES_KILLED, entry.ioUsage.getTotalTimesKilled());
             values.put(
                     COLUMN_WRITTEN_FOREGROUND_BYTES, ioOveruseStats.writtenBytes.foregroundBytes);
@@ -664,6 +769,7 @@
                     .append(COLUMN_USER_PACKAGE_ID).append(", ")
                     .append("MIN(").append(COLUMN_DATE_EPOCH).append("), ")
                     .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
+                    .append("SUM(").append(COLUMN_NUM_FORGIVEN_OVERUSES).append("), ")
                     .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ")
                     .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
                     .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
@@ -691,20 +797,22 @@
                             excludingEndEpochSeconds - includingStartEpochSeconds;
                     ioOveruseStats.totalOveruses = cursor.getInt(2);
                     ioOveruseStats.writtenBytes = new PerStateBytes();
-                    ioOveruseStats.writtenBytes.foregroundBytes = cursor.getLong(4);
-                    ioOveruseStats.writtenBytes.backgroundBytes = cursor.getLong(5);
-                    ioOveruseStats.writtenBytes.garageModeBytes = cursor.getLong(6);
+                    ioOveruseStats.writtenBytes.foregroundBytes = cursor.getLong(5);
+                    ioOveruseStats.writtenBytes.backgroundBytes = cursor.getLong(6);
+                    ioOveruseStats.writtenBytes.garageModeBytes = cursor.getLong(7);
                     ioOveruseStats.remainingWriteBytes = new PerStateBytes();
-                    ioOveruseStats.remainingWriteBytes.foregroundBytes = cursor.getLong(7);
-                    ioOveruseStats.remainingWriteBytes.backgroundBytes = cursor.getLong(8);
-                    ioOveruseStats.remainingWriteBytes.garageModeBytes = cursor.getLong(9);
+                    ioOveruseStats.remainingWriteBytes.foregroundBytes = cursor.getLong(8);
+                    ioOveruseStats.remainingWriteBytes.backgroundBytes = cursor.getLong(9);
+                    ioOveruseStats.remainingWriteBytes.garageModeBytes = cursor.getLong(10);
                     PerStateBytes forgivenWriteBytes = new PerStateBytes();
-                    forgivenWriteBytes.foregroundBytes = cursor.getLong(10);
-                    forgivenWriteBytes.backgroundBytes = cursor.getLong(11);
-                    forgivenWriteBytes.garageModeBytes = cursor.getLong(12);
+                    forgivenWriteBytes.foregroundBytes = cursor.getLong(11);
+                    forgivenWriteBytes.backgroundBytes = cursor.getLong(12);
+                    forgivenWriteBytes.garageModeBytes = cursor.getLong(13);
 
                     ioUsageById.put(cursor.getString(0), new WatchdogPerfHandler.PackageIoUsage(
-                            ioOveruseStats, forgivenWriteBytes, cursor.getInt(3)));
+                            ioOveruseStats, forgivenWriteBytes,
+                            /* forgivenOveruses= */ cursor.getInt(3),
+                            /* totalTimesKilled= */ cursor.getInt(4)));
                 }
             }
             return ioUsageById;
@@ -753,6 +861,63 @@
             return statsBuilder.build();
         }
 
+        public static ArrayMap<String, Integer> queryNotForgivenHistoricalOveruses(
+                SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds) {
+            StringBuilder queryBuilder = new StringBuilder("SELECT ")
+                    .append(COLUMN_USER_PACKAGE_ID).append(", ")
+                    .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
+                    .append("SUM(").append(COLUMN_NUM_FORGIVEN_OVERUSES).append(") ")
+                    .append("FROM ").append(TABLE_NAME).append(" WHERE ")
+                    .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
+                    .append(COLUMN_DATE_EPOCH).append("< ? GROUP BY ")
+                    .append(COLUMN_USER_PACKAGE_ID);
+            String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds),
+                    String.valueOf(excludingEndEpochSeconds)};
+            ArrayMap<String, Integer> notForgivenOverusesById = new ArrayMap<>();
+            try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
+                while (cursor.moveToNext()) {
+                    if (cursor.getInt(1) <= cursor.getInt(2)) {
+                        continue;
+                    }
+                    notForgivenOverusesById.put(cursor.getString(0),
+                            cursor.getInt(1) - cursor.getInt(2));
+                }
+            }
+            return notForgivenOverusesById;
+        }
+
+        public static void forgiveHistoricalOverusesForPackage(SQLiteDatabase db,
+                List<String> uniqueIds, long includingStartEpochSeconds,
+                long excludingEndEpochSeconds) {
+            if (uniqueIds.isEmpty()) {
+                Slogf.e(TAG, "No unique ids provided to forgive historical overuses.");
+                return;
+            }
+            StringBuilder updateQueryBuilder = new StringBuilder("UPDATE ").append(TABLE_NAME)
+                    .append(" SET ")
+                    .append(COLUMN_NUM_FORGIVEN_OVERUSES).append("=").append(COLUMN_NUM_OVERUSES)
+                    .append(" WHERE ")
+                    .append(COLUMN_DATE_EPOCH).append(">= ").append(includingStartEpochSeconds)
+                    .append(" and ")
+                    .append(COLUMN_DATE_EPOCH).append("< ").append(excludingEndEpochSeconds);
+            for (int i = 0; i < uniqueIds.size(); i++) {
+                if (i == 0) {
+                    updateQueryBuilder.append(" and ").append(COLUMN_USER_PACKAGE_ID)
+                            .append(" IN (");
+                } else {
+                    updateQueryBuilder.append(", ");
+                }
+                updateQueryBuilder.append(uniqueIds.get(i));
+                if (i == uniqueIds.size() - 1) {
+                    updateQueryBuilder.append(")");
+                }
+            }
+
+            db.execSQL(updateQueryBuilder.toString());
+            Slogf.i(TAG, "Attempted to forgive overuses for I/O usage stats entries on pid %d",
+                    Process.myPid());
+        }
+
         public static @Nullable List<AtomsProto.CarWatchdogDailyIoUsageSummary>
                 queryDailySystemIoUsageSummaries(SQLiteDatabase db, long includingStartEpochSeconds,
                 long excludingEndEpochSeconds) {
diff --git a/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java b/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
index ca2faf4..30b8fa1 100644
--- a/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
@@ -40,6 +40,7 @@
 import android.car.watchdog.CarWatchdogManager;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.content.res.Resources;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -80,6 +81,8 @@
     private static final String CAR_WATCHDOG_DAEMON_INTERFACE = "carwatchdogd_system";
     private static final int MAX_WAIT_TIME_MS = 3000;
     private static final int INVALID_SESSION_ID = -1;
+    private static final int RECURRING_OVERUSE_TIMES = 2;
+    private static final int RECURRING_OVERUSE_PERIOD_IN_DAYS = 2;
 
     private final Handler mMainHandler = new Handler(Looper.getMainLooper());
     private final Executor mExecutor =
@@ -92,6 +95,7 @@
 
     @Mock private Context mMockContext;
     @Mock private Car mMockCar;
+    @Mock private Resources mMockResources;
     @Mock private UserManager mMockUserManager;
     @Mock private StatsManager mMockStatsManager;
     @Mock private SystemInterface mMockSystemInterface;
@@ -108,14 +112,17 @@
 
     @Before
     public void setUp() throws Exception {
-        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage,
-                mMockUserNotificationHelper, mTimeSource);
-
         mockQueryService(CAR_WATCHDOG_DAEMON_INTERFACE, mMockDaemonBinder, mMockCarWatchdogDaemon);
         when(mMockCar.getEventHandler()).thenReturn(mMainHandler);
-        when(mMockServiceBinder.queryLocalInterface(anyString())).thenReturn(mCarWatchdogService);
         when(mMockContext.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager);
         when(mMockContext.getSystemService(StatsManager.class)).thenReturn(mMockStatsManager);
+        when(mMockContext.getResources()).thenReturn(mMockResources);
+        when(mMockResources.getInteger(
+                com.android.car.R.integer.recurringResourceOverusePeriodInDays))
+                .thenReturn(RECURRING_OVERUSE_PERIOD_IN_DAYS);
+        when(mMockResources.getInteger(
+                com.android.car.R.integer.recurringResourceOveruseTimes))
+                .thenReturn(RECURRING_OVERUSE_TIMES);
 
         doReturn(mMockSystemInterface)
                 .when(() -> CarLocalServices.getService(SystemInterface.class));
@@ -128,6 +135,11 @@
         mockUmIsUserRunning(mMockUserManager, 100, true);
         mockUmIsUserRunning(mMockUserManager, 101, false);
 
+        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage,
+                mMockUserNotificationHelper, mTimeSource);
+
+        when(mMockServiceBinder.queryLocalInterface(anyString())).thenReturn(mCarWatchdogService);
+
         mCarWatchdogService.init();
         mWatchdogServiceForSystemImpl = registerCarWatchdogService();
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
index 6215a6a..5a745e9 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
@@ -117,6 +117,7 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.content.res.Resources;
 import android.os.Binder;
 import android.os.FileUtils;
 import android.os.Handler;
@@ -179,7 +180,8 @@
     private static final int MAX_WAIT_TIME_MS = 3000;
     private static final int INVALID_SESSION_ID = -1;
     private static final int OVERUSE_HANDLING_DELAY_MILLS = 1000;
-    private static final int RECURRING_OVERUSE_THRESHOLD = 2;
+    private static final int RECURRING_OVERUSE_TIMES = 2;
+    private static final int RECURRING_OVERUSE_PERIOD_IN_DAYS = 2;
     private static final int UID_IO_USAGE_SUMMARY_TOP_COUNT = 3;
     private static final long STATS_DURATION_SECONDS = 3 * 60 * 60;
     private static final long SYSTEM_DAILY_IO_USAGE_SUMMARY_MULTIPLIER = 10_000;
@@ -192,6 +194,7 @@
     @Mock private SystemInterface mMockSystemInterface;
     @Mock private CarPowerManagementService mMockCarPowerManagementService;
     @Mock private CarUxRestrictionsManagerService mMockCarUxRestrictionsManagerService;
+    @Mock private Resources mMockResources;
     @Mock private IBinder mMockBinder;
     @Mock private ICarWatchdog mMockCarWatchdogDaemon;
     @Mock private WatchdogStorage mMockWatchdogStorage;
@@ -208,6 +211,7 @@
     @Captor private ArgumentCaptor<List<
             android.automotive.watchdog.internal.ResourceOveruseConfiguration>>
             mResourceOveruseConfigurationsCaptor;
+    @Captor private ArgumentCaptor<SparseArray<List<String>>> mPackagesByUserIdCaptor;
     @Captor private ArgumentCaptor<StatsPullAtomCallback> mStatsPullAtomCallbackCaptor;
     @Captor private ArgumentCaptor<List<UserNotificationHelper.PackageNotificationInfo>>
             mPackageNotificationInfosCaptor;
@@ -270,6 +274,13 @@
         when(mMockContext.getSystemService(StatsManager.class)).thenReturn(mMockStatsManager);
         when(mMockContext.getPackageName()).thenReturn(
                 CarWatchdogServiceUnitTest.class.getCanonicalName());
+        when(mMockContext.getResources()).thenReturn(mMockResources);
+        when(mMockResources.getInteger(
+                eq(com.android.car.R.integer.recurringResourceOverusePeriodInDays)))
+                .thenReturn(RECURRING_OVERUSE_PERIOD_IN_DAYS);
+        when(mMockResources.getInteger(
+                eq(com.android.car.R.integer.recurringResourceOveruseTimes)))
+                .thenReturn(RECURRING_OVERUSE_TIMES);
         doReturn(mMockSystemInterface)
                 .when(() -> CarLocalServices.getService(SystemInterface.class));
         doReturn(mMockCarPowerManagementService)
@@ -2182,13 +2193,15 @@
                         /* remainingWriteBytes= */ constructPerStateBytes(200, 300, 400),
                         /* writtenBytes= */ constructPerStateBytes(1000, 2000, 3000),
                         /* forgivenWriteBytes= */ constructPerStateBytes(100, 100, 100),
-                        /* totalOveruses= */ 2, /* totalTimesKilled= */ 1),
+                        /* totalOveruses= */ 2, /* forgivenOveruses= */ 0,
+                        /* totalTimesKilled= */ 1),
                 WatchdogStorageUnitTest.constructIoUsageStatsEntry(
                         /* userId= */ 11, "vendor_package", /* startTime */ 0, /* duration= */ 1234,
                         /* remainingWriteBytes= */ constructPerStateBytes(500, 600, 700),
                         /* writtenBytes= */ constructPerStateBytes(1100, 2300, 4300),
                         /* forgivenWriteBytes= */ constructPerStateBytes(100, 100, 100),
-                        /* totalOveruses= */ 4, /* totalTimesKilled= */ 10));
+                        /* totalOveruses= */ 4, /* forgivenOveruses= */ 1,
+                        /* totalTimesKilled= */ 10));
         when(mMockWatchdogStorage.getTodayIoUsageStats()).thenReturn(ioUsageStatsEntries);
 
         List<UserPackageIoUsageStats> actualStats =
@@ -2292,11 +2305,11 @@
                 new WatchdogStorage.IoUsageStatsEntry(/* userId= */ 10, "system_package",
                 new WatchdogPerfHandler.PackageIoUsage(prevDayStats.get(0).ioOveruseStats,
                         /* forgivenWriteBytes= */ constructPerStateBytes(600, 700, 800),
-                        /* totalTimesKilled= */ 1)),
+                        /* forgivenOveruses= */ 3, /* totalTimesKilled= */ 1)),
                 new WatchdogStorage.IoUsageStatsEntry(/* userId= */ 10, "third_party_package",
                         new WatchdogPerfHandler.PackageIoUsage(prevDayStats.get(1).ioOveruseStats,
                                 /* forgivenWriteBytes= */ constructPerStateBytes(1050, 1100, 1200),
-                                /* totalTimesKilled= */ 0)));
+                                /* forgivenOveruses= */ 0, /* totalTimesKilled= */ 0)));
 
         setDisplayStateEnabled(true);
         mTimeSource.updateNow(/* numDaysAgo= */ 0);
@@ -2523,19 +2536,35 @@
                         /* notificationIds= */ Arrays.asList(150, 151))));
     }
 
-    /* TODO(b/195425666): Test resource overuse notifications taking into considerations
-     *  historical overuses.
-     *
-     *  @Test
-     *  public void testUserNotificationOnHistoricalRecurrentOveruse() throws Exception {
-     *    1. Setup historical stats with RECURRING_OVERUSE_THRESHOLD overuses by mocking
-     *       WatchdogStorage calls.
-     *    2. Set display to enabled and UX restriction to requires distraction optimization.
-     *    3. Push some I/O stats with non-recurrent overuse and wait.
-     *    4. Set UX restriction to not require distraction optimization.
-     *    5. Verify no user notification.
-     *  }
-     */
+    @Test
+    public void testUserNotificationOnHistoricalRecurrentOveruse() throws Exception {
+        when(mMockWatchdogStorage
+                .getNotForgivenHistoricalIoOveruses(RECURRING_OVERUSE_PERIOD_IN_DAYS))
+                .thenReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
+                        "system_package.non_critical", 2)));
+
+        // Force CarWatchdogService to fetch historical not forgiven overuses.
+        restartService(/* totalRestarts= */ 1);
+        mockAmGetCurrentUser(100);
+        setDisplayStateEnabled(true);
+        setRequiresDistractionOptimization(false);
+
+        setUpSampleUserAndPackages();
+
+        pushLatestIoOveruseStatsAndWait(Collections.singletonList(
+                constructPackageIoOveruseStats(/* uid= */ 10010002, /* shouldNotify= */ true,
+                        /* forgivenWriteBytes= */ constructPerStateBytes(100, 200, 300),
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(300, 600, 900),
+                                /* totalOveruses= */ 1))));
+
+        captureAndVerifyUserNotifications(Collections.singletonList(
+                new UserNotificationCall(UserHandle.of(100),
+                        Collections.singletonList("system_package.non_critical"),
+                        /* hasHeadsUpNotification= */ true,
+                        /* notificationIds= */ Collections.singletonList(150))));
+    }
 
     @Test
     public void testUserNotificationWithDisabledDisplay() throws Exception {
@@ -2830,10 +2859,106 @@
                 "100:third_party_package.B", "101:third_party_package.B");
     }
 
-    /* TODO(b/195425666): Test recurrently overusing app with overuse history stored in the DB.
-     * @Test
-     * public void  testDisableHistoricalRecurrentlyOverusingApp() throws Exception {}
-     */
+    @Test
+    public void testDisableHistoricalRecurrentlyOverusingApp() throws Exception {
+        when(mMockWatchdogStorage
+                .getNotForgivenHistoricalIoOveruses(RECURRING_OVERUSE_PERIOD_IN_DAYS))
+                .thenReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
+                        "third_party_package", 2)));
+
+        // Force CarWatchdogService to fetch historical not forgiven overuses.
+        restartService(/* totalRestarts= */ 1);
+        setRequiresDistractionOptimization(true);
+        setDisplayStateEnabled(false);
+        int thirdPartyPkgUid = UserHandle.getUid(100, 10005);
+
+        injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+                "third_party_package", thirdPartyPkgUid, null)));
+
+        pushLatestIoOveruseStatsAndWait(
+                sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ false));
+
+        // Third party package is disabled given the two historical overuses and one current
+        // overuse.
+        assertWithMessage("Disabled packages after recurring overuse with history")
+                .that(mDisabledUserPackages)
+                .containsExactlyElementsIn(Collections.singleton("100:third_party_package"));
+
+        // Package was enabled again.
+        mDisabledUserPackages.clear();
+
+        PackageIoOveruseStats packageIoOveruseStats =
+                constructPackageIoOveruseStats(thirdPartyPkgUid, /* shouldNotify= */ true,
+                        /* forgivenWriteBytes= */ constructPerStateBytes(200, 400, 600),
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(200, 400, 600),
+                                /* totalOveruses= */ 3));
+
+        pushLatestIoOveruseStatsAndWait(Collections.singletonList(packageIoOveruseStats));
+
+        // From the 3 total overuses, one overuse was forgiven previously.
+        assertWithMessage("Disabled packages after non-recurring overuse")
+                .that(mDisabledUserPackages).isEmpty();
+
+        // Add one overuse.
+        packageIoOveruseStats.ioOveruseStats.totalOveruses = 4;
+
+        pushLatestIoOveruseStatsAndWait(Collections.singletonList(packageIoOveruseStats));
+
+        // Third party package is disabled again given the three current overuses. From the 4 total
+        // overuses, one overuse was forgiven previously.
+        assertWithMessage("Disabled packages after recurring overuse from the same day")
+                .that(mDisabledUserPackages)
+                .containsExactlyElementsIn(Collections.singleton("100:third_party_package"));
+
+        // Force write to database
+        restartService(/* totalRestarts= */ 2);
+
+        verify(mMockWatchdogStorage).forgiveHistoricalOveruses(mPackagesByUserIdCaptor.capture(),
+                eq(RECURRING_OVERUSE_PERIOD_IN_DAYS));
+
+        assertWithMessage("Forgiven packages")
+                .that(mPackagesByUserIdCaptor.getValue().get(100))
+                .containsExactlyElementsIn(Arrays.asList("third_party_package"));
+    }
+
+    @Test
+    public void testDisableHistoricalRecurrentlyOverusingAppAfterDateChange() throws Exception {
+        when(mMockWatchdogStorage.getNotForgivenHistoricalIoOveruses(
+                eq(RECURRING_OVERUSE_PERIOD_IN_DAYS)))
+                .thenReturn(Arrays.asList(new WatchdogStorage.NotForgivenOverusesEntry(100,
+                        "third_party_package", 2)));
+
+        mTimeSource.updateNow(/* numDaysAgo= */ 1);
+        setRequiresDistractionOptimization(true);
+        setDisplayStateEnabled(false);
+        int thirdPartyPkgUid = UserHandle.getUid(100, 10005);
+
+        injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+                "third_party_package", thirdPartyPkgUid, null)));
+
+        List<PackageIoOveruseStats> ioOveruseStats =
+                sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ false);
+        pushLatestIoOveruseStatsAndWait(ioOveruseStats);
+
+        // Third party package is disabled given the two historical overuses and one current
+        // overuse.
+        assertThat(mDisabledUserPackages)
+                .containsExactlyElementsIn(Collections.singleton("100:third_party_package"));
+
+        // Force write to database by pushing non-overusing I/O overuse stats.
+        mTimeSource.updateNow(/* numDaysAgo= */ 0);
+        pushLatestIoOveruseStatsAndWait(Collections.singletonList(ioOveruseStats.get(0)));
+
+        verify(mMockWatchdogStorage).forgiveHistoricalOveruses(mPackagesByUserIdCaptor.capture(),
+                eq(RECURRING_OVERUSE_PERIOD_IN_DAYS));
+
+        assertWithMessage("Forgiven packages")
+                .that(mPackagesByUserIdCaptor.getValue().get(100))
+                .containsExactlyElementsIn(Arrays.asList("third_party_package"));
+    }
+
 
     @Test
     public void testResetResourceOveruseStatsResetsStats() throws Exception {
@@ -3455,6 +3580,7 @@
                                 new WatchdogPerfHandler.PackageIoUsage(
                                         entry.ioUsage.getInternalIoOveruseStats(),
                                         entry.ioUsage.getForgivenWriteBytes(),
+                                        entry.ioUsage.getForgivenOveruses(),
                                         entry.ioUsage.getTotalTimesKilled())));
             }
             return true;
@@ -3489,7 +3615,6 @@
     private void initService(int wantedInvocations) throws Exception {
         mTimeSource.updateNow(/* numDaysAgo= */ 0);
         mCarWatchdogService.setOveruseHandlingDelay(OVERUSE_HANDLING_DELAY_MILLS);
-        mCarWatchdogService.setRecurringOveruseThreshold(RECURRING_OVERUSE_THRESHOLD);
         mCarWatchdogService.setUidIoUsageSummaryTopCount(
                 UID_IO_USAGE_SUMMARY_TOP_COUNT);
         mCarWatchdogService.init();
@@ -3584,6 +3709,8 @@
         verify(mMockWatchdogStorage, times(wantedInvocations)).syncUsers(any());
         verify(mMockWatchdogStorage, times(wantedInvocations)).getUserPackageSettings();
         verify(mMockWatchdogStorage, times(wantedInvocations)).getTodayIoUsageStats();
+        verify(mMockWatchdogStorage, times(wantedInvocations)).getNotForgivenHistoricalIoOveruses(
+                RECURRING_OVERUSE_PERIOD_IN_DAYS);
     }
 
     private void captureStatsPullAtomCallback(int wantedInvocations) {
@@ -3842,7 +3969,7 @@
     private List<PackageIoOveruseStats> sampleIoOveruseStats(boolean requireRecurrentOveruseStats)
             throws Exception {
         int[] users = new int[]{100, 101};
-        int totalOveruses = requireRecurrentOveruseStats ? RECURRING_OVERUSE_THRESHOLD + 1 : 0;
+        int totalOveruses = requireRecurrentOveruseStats ? RECURRING_OVERUSE_TIMES + 1 : 1;
         List<PackageIoOveruseStats> packageIoOveruseStats = new ArrayList<>();
         android.automotive.watchdog.PerStateBytes zeroRemainingBytes =
                 constructPerStateBytes(0, 0, 0);
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java
index 0479c93..4e08b0f 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java
@@ -79,6 +79,7 @@
         }
         return actual.userId == expected.userId && actual.packageName.equals(expected.packageName)
                 && actual.ioUsage.getTotalTimesKilled() == expected.ioUsage.getTotalTimesKilled()
+                && actual.ioUsage.getForgivenOveruses() == expected.ioUsage.getForgivenOveruses()
                 && InternalPerStateBytesSubject.isEquals(actual.ioUsage.getForgivenWriteBytes(),
                 expected.ioUsage.getForgivenWriteBytes())
                 && isEqualsIoOveruseStats(actual.ioUsage.getInternalIoOveruseStats(),
@@ -127,6 +128,7 @@
         toStringBuilder(builder, ioUsage.getInternalIoOveruseStats());
         builder.append(", ForgivenWriteBytes: ");
         InternalPerStateBytesSubject.toStringBuilder(builder, ioUsage.getForgivenWriteBytes());
+        builder.append(", Forgiven overuses: ").append(ioUsage.getForgivenOveruses());
         return builder.append(", Total times killed: ").append(ioUsage.getTotalTimesKilled())
                 .append('}');
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java b/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
index 85a54ec..84b8672 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
@@ -31,6 +31,7 @@
 import android.car.watchdog.IoOveruseStats;
 import android.content.Context;
 import android.util.Slog;
+import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -167,7 +168,8 @@
                         CarWatchdogServiceUnitTest.constructPerStateBytes(1000, 2000, 3000),
                         /* forgivenWriteBytes= */
                         CarWatchdogServiceUnitTest.constructPerStateBytes(100, 100, 100),
-                        /* totalOveruses= */ 2, /* totalTimesKilled= */ 1));
+                        /* totalOveruses= */ 2, /* forgivenOveruses= */ 0,
+                        /* totalTimesKilled= */ 1));
 
         assertWithMessage("Saved I/O usage stats successfully")
                 .that(mService.saveIoUsageStats(statsBeforeOverwrite)).isTrue();
@@ -185,7 +187,8 @@
                         CarWatchdogServiceUnitTest.constructPerStateBytes(2000, 3000, 4000),
                         /* forgivenWriteBytes= */
                         CarWatchdogServiceUnitTest.constructPerStateBytes(1200, 2300, 3400),
-                        /* totalOveruses= */ 4, /* totalTimesKilled= */ 2));
+                        /* totalOveruses= */ 4, /* forgivenOveruses= */ 2,
+                        /* totalTimesKilled= */ 2));
 
         assertWithMessage("Saved I/O usage stats successfully")
                 .that(mService.saveIoUsageStats(statsAfterOverwrite)).isTrue();
@@ -604,6 +607,42 @@
                 expected.toString(), actual.toString()).that(actual).isEqualTo(expected);
     }
 
+    @Test
+    public void testForgiveHistoricalOveruses() throws Exception {
+        injectSampleUserPackageSettings();
+
+        assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(/* includingStartDaysAgo= */ 1,
+                /* excludingEndDaysAgo= */ 3))).isTrue();
+
+        List<WatchdogStorage.NotForgivenOverusesEntry> expectedOveruses = Arrays.asList(
+                new WatchdogStorage.NotForgivenOverusesEntry(100, "system_package.non_critical.A",
+                        2),
+                new WatchdogStorage.NotForgivenOverusesEntry(101, "system_package.non_critical.A",
+                        2),
+                new WatchdogStorage.NotForgivenOverusesEntry(100, "vendor_package.critical.C", 2),
+                new WatchdogStorage.NotForgivenOverusesEntry(101, "vendor_package.critical.C", 2));
+
+        assertWithMessage("Not forgiven historical overuses before forgiving")
+                .that(mService.getNotForgivenHistoricalIoOveruses(/* numDaysAgo= */ 7))
+                .containsExactlyElementsIn(expectedOveruses);
+
+        SparseArray<List<String>> packagesToForgiveByUserId = new SparseArray<>();
+        packagesToForgiveByUserId.put(100,
+                Collections.singletonList("system_package.non_critical.A"));
+        packagesToForgiveByUserId.put(101, Collections.singletonList("vendor_package.critical.C"));
+
+        mService.forgiveHistoricalOveruses(packagesToForgiveByUserId, /* numDaysAgo= */ 7);
+
+        expectedOveruses = Arrays.asList(
+                new WatchdogStorage.NotForgivenOverusesEntry(101, "system_package.non_critical.A",
+                        2),
+                new WatchdogStorage.NotForgivenOverusesEntry(100, "vendor_package.critical.C", 2));
+
+        assertWithMessage("Not forgiven historical overuses after forgiving")
+                .that(mService.getNotForgivenHistoricalIoOveruses(/* numDaysAgo= */ 7))
+                .containsExactlyElementsIn(expectedOveruses);
+    }
+
     private void injectSampleUserPackageSettings() throws Exception {
         List<WatchdogStorage.UserPackageSettingsEntry> expected = sampleSettings();
 
@@ -670,7 +709,7 @@
                             (3000L + i) * writtenBytesMultiplier),
                     /* forgivenWriteBytes= */
                     CarWatchdogServiceUnitTest.constructPerStateBytes(100L, 100L, 100L),
-                    /* totalOveruses= */ 2, /* totalTimesKilled= */ 1));
+                    /* totalOveruses= */ 2, /* forgivenOveruses= */ 1, /* totalTimesKilled= */ 1));
             entries.add(constructIoUsageStatsEntry(
                     /* userId= */ i, "vendor_package.critical.C", statsDateEpoch, duration,
                     /* remainingWriteBytes= */
@@ -682,7 +721,7 @@
                             (6000L + i) * writtenBytesMultiplier),
                     /* forgivenWriteBytes= */
                     CarWatchdogServiceUnitTest.constructPerStateBytes(200L, 200L, 200L),
-                    /* totalOveruses= */ 1, /* totalTimesKilled= */ 0));
+                    /* totalOveruses= */ 1, /* forgivenOveruses= */ 0, /* totalTimesKilled= */ 0));
         }
         return entries;
     }
@@ -690,10 +729,12 @@
     static WatchdogStorage.IoUsageStatsEntry constructIoUsageStatsEntry(
             int userId, String packageName, long startTime, long duration,
             PerStateBytes remainingWriteBytes, PerStateBytes writtenBytes,
-            PerStateBytes forgivenWriteBytes, int totalOveruses, int totalTimesKilled) {
+            PerStateBytes forgivenWriteBytes, int totalOveruses, int forgivenOveruses,
+            int totalTimesKilled) {
         WatchdogPerfHandler.PackageIoUsage ioUsage = new WatchdogPerfHandler.PackageIoUsage(
                 constructInternalIoOveruseStats(startTime, duration, remainingWriteBytes,
-                        writtenBytes, totalOveruses), forgivenWriteBytes, totalTimesKilled);
+                        writtenBytes, totalOveruses), forgivenWriteBytes, forgivenOveruses,
+                totalTimesKilled);
         return new WatchdogStorage.IoUsageStatsEntry(userId, packageName, ioUsage);
     }