Merge changes from topic "PullIoOveruseStats-sc-v2-dev" into sc-v2-dev

* changes:
  Initialize CarWatchdog's UX state during init.
  Pull weekly I/O usage summary metrics from CarService.
  Add watchdog storage queries to fetch I/O usage summary stats.
diff --git a/service/src/com/android/car/watchdog/CarWatchdogService.java b/service/src/com/android/car/watchdog/CarWatchdogService.java
index be888d3..9347a83 100644
--- a/service/src/com/android/car/watchdog/CarWatchdogService.java
+++ b/service/src/com/android/car/watchdog/CarWatchdogService.java
@@ -62,12 +62,14 @@
 import com.android.car.CarServiceUtils;
 import com.android.car.ICarImpl;
 import com.android.car.power.CarPowerManagementService;
+import com.android.car.systeminterface.SystemInterface;
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.utils.Slogf;
 
+import java.io.File;
 import java.lang.ref.WeakReference;
 import java.time.Instant;
 import java.util.List;
@@ -83,7 +85,11 @@
     static final String ACTION_GARAGE_MODE_OFF =
             "com.android.server.jobscheduler.GARAGE_MODE_OFF";
     static final int MISSING_ARG_VALUE = -1;
-    static final TimeSourceInterface SYSTEM_INSTANCE = new TimeSourceInterface() {
+
+    private static final String FALLBACK_DATA_SYSTEM_CAR_DIR_PATH = "/data/system/car";
+    private static final String WATCHDOG_DIR_NAME = "watchdog";
+
+    private static final TimeSource SYSTEM_INSTANCE = new TimeSource() {
         @Override
         public Instant now() {
             return Instant.now();
@@ -145,7 +151,14 @@
             }
             int powerCycle = carPowerStateToPowerCycle(powerService.getPowerState());
             switch (powerCycle) {
+                case PowerCycle.POWER_CYCLE_SHUTDOWN_PREPARE:
+                    // Perform time consuming disk I/O operation during shutdown prepare to avoid
+                    // incomplete I/O.
+                    mWatchdogPerfHandler.writeMetadataFile();
+                    break;
                 case PowerCycle.POWER_CYCLE_SHUTDOWN_ENTER:
+                    // Watchdog service and daemon performs garage mode monitoring so delay writing
+                    // to database until after shutdown enter.
                     mWatchdogPerfHandler.writeToDatabase();
                     break;
                 // ON covers resume.
@@ -190,11 +203,11 @@
     private boolean mIsDisplayEnabled;
 
     public CarWatchdogService(Context context) {
-        this(context, new WatchdogStorage(context));
+        this(context, new WatchdogStorage(context, SYSTEM_INSTANCE), SYSTEM_INSTANCE);
     }
 
     @VisibleForTesting
-    CarWatchdogService(Context context, WatchdogStorage watchdogStorage) {
+    CarWatchdogService(Context context, WatchdogStorage watchdogStorage, TimeSource timeSource) {
         mContext = context;
         mWatchdogStorage = watchdogStorage;
         mPackageInfoHandler = new PackageInfoHandler(mContext.getPackageManager());
@@ -203,7 +216,7 @@
         mWatchdogProcessHandler = new WatchdogProcessHandler(mWatchdogServiceForSystem,
                 mCarWatchdogDaemonHelper);
         mWatchdogPerfHandler = new WatchdogPerfHandler(mContext, mCarWatchdogDaemonHelper,
-                mPackageInfoHandler, mWatchdogStorage);
+                mPackageInfoHandler, mWatchdogStorage, timeSource);
         mConnectionListener = (isConnected) -> {
             mWatchdogPerfHandler.onDaemonConnectionChange(isConnected);
             synchronized (mLock) {
@@ -420,11 +433,6 @@
     }
 
     @VisibleForTesting
-    void setTimeSource(TimeSourceInterface timeSource) {
-        mWatchdogPerfHandler.setTimeSource(timeSource);
-    }
-
-    @VisibleForTesting
     void setOveruseHandlingDelay(long millis) {
         mWatchdogPerfHandler.setOveruseHandlingDelay(millis);
     }
@@ -434,6 +442,18 @@
         mWatchdogPerfHandler.setRecurringOveruseThreshold(threshold);
     }
 
+    @VisibleForTesting
+    void setUidIoUsageSummaryTopCount(int uidIoUsageSummaryTopCount) {
+        mWatchdogPerfHandler.setUidIoUsageSummaryTopCount(uidIoUsageSummaryTopCount);
+    }
+
+    static File getWatchdogDirFile() {
+        SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class);
+        String systemCarDirPath = systemInterface == null ? FALLBACK_DATA_SYSTEM_CAR_DIR_PATH
+                : systemInterface.getSystemCarDir().getAbsolutePath();
+        return new File(systemCarDirPath, WATCHDOG_DIR_NAME);
+    }
+
     private void notifyAllUserStates() {
         UserManager userManager = UserManager.get(mContext);
         List<UserInfo> users = userManager.getUsers();
diff --git a/service/src/com/android/car/watchdog/TimeSource.java b/service/src/com/android/car/watchdog/TimeSource.java
new file mode 100644
index 0000000..6537ce2
--- /dev/null
+++ b/service/src/com/android/car/watchdog/TimeSource.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 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.car.watchdog;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+
+/**
+ * Provider for the current value of "now" for users of {@code java.time}.
+ */
+public abstract class TimeSource {
+    public static final ZoneOffset ZONE_OFFSET = ZoneOffset.UTC;
+
+    /** Returns the current instant from the time source implementation. */
+    abstract Instant now();
+
+    /** Returns the current date time for the UTC timezone. */
+    public ZonedDateTime getCurrentDateTime() {
+        return now().atZone(ZONE_OFFSET);
+    }
+
+    /** Returns the current date for the UTC timezone without time. */
+    public ZonedDateTime getCurrentDate() {
+        return now().atZone(ZONE_OFFSET).truncatedTo(ChronoUnit.DAYS);
+    }
+}
diff --git a/service/src/com/android/car/watchdog/TimeSourceInterface.java b/service/src/com/android/car/watchdog/TimeSourceInterface.java
deleted file mode 100644
index 1bd6508..0000000
--- a/service/src/com/android/car/watchdog/TimeSourceInterface.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2021 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.car.watchdog;
-
-import java.time.Instant;
-
-/**
- * Provider for the current value of "now" for users of {@code java.time}.
- */
-public interface TimeSourceInterface {
-    /** Returns the current instant from the time source implementation. */
-    Instant now();
-}
diff --git a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
index 0fcaeb0..6103560 100644
--- a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+++ b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
@@ -16,6 +16,8 @@
 
 package com.android.car.watchdog;
 
+import static android.app.StatsManager.PULL_SKIP;
+import static android.app.StatsManager.PULL_SUCCESS;
 import static android.car.watchdog.CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO;
 import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_CURRENT_DAY;
 import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_PAST_15_DAYS;
@@ -38,18 +40,20 @@
 import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_INTERACTION_MODE;
 import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE;
 import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__UID_STATE__UNKNOWN_UID_STATE;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_UID_IO_USAGE_SUMMARY;
 import static com.android.car.watchdog.CarWatchdogService.DEBUG;
-import static com.android.car.watchdog.CarWatchdogService.SYSTEM_INSTANCE;
 import static com.android.car.watchdog.CarWatchdogService.TAG;
 import static com.android.car.watchdog.PackageInfoHandler.SHARED_PACKAGE_PREFIX;
-import static com.android.car.watchdog.WatchdogStorage.STATS_TEMPORAL_UNIT;
-import static com.android.car.watchdog.WatchdogStorage.ZONE_OFFSET;
+import static com.android.car.watchdog.TimeSource.ZONE_OFFSET;
+import static com.android.car.watchdog.WatchdogStorage.RETENTION_PERIOD;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.ActivityThread;
-import android.automotive.watchdog.ResourceType;
+import android.app.StatsManager;
+import android.app.StatsManager.PullAtomMetadata;
 import android.automotive.watchdog.internal.ApplicationCategoryType;
 import android.automotive.watchdog.internal.ComponentType;
 import android.automotive.watchdog.internal.GarageMode;
@@ -88,8 +92,13 @@
 import android.os.UserManager;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Pair;
 import android.util.SparseArray;
+import android.util.StatsEvent;
 import android.view.Display;
 
 import com.android.car.CarLocalServices;
@@ -98,19 +107,32 @@
 import com.android.car.CarUxRestrictionsManagerService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ConcurrentUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.utils.Slogf;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.time.ZoneOffset;
+import java.nio.charset.StandardCharsets;
 import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 
@@ -121,10 +143,26 @@
     public static final String INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS = "MAPS";
     public static final String INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA = "MEDIA";
     public static final String INTERNAL_APPLICATION_CATEGORY_TYPE_UNKNOWN = "UNKNOWN";
-
+    public static final int UID_IO_USAGE_SUMMARY_TOP_COUNT = 10;
+    public static final int UID_IO_USAGE_SUMMARY_MIN_WEEKLY_WRITTEN_BYTES = 500 * 1024 * 1024;
+    private static final String METADATA_FILENAME = "metadata.json";
+    private static final String SYSTEM_IO_USAGE_SUMMARY_REPORTED_DATE =
+            "systemIoUsageSummaryReportedDate";
+    private static final String UID_IO_USAGE_SUMMARY_REPORTED_DATE =
+            "uidIoUsageSummaryReportedDate";
     private static final long OVERUSE_HANDLING_DELAY_MILLS = 10_000;
     private static final long MAX_WAIT_TIME_MILLS = 3_000;
 
+    private static final PullAtomMetadata PULL_ATOM_METADATA =
+            new PullAtomMetadata.Builder()
+                    // Summary atoms are populated only once a week. So, a longer cool down duration
+                    // is sufficient.
+                    .setCoolDownMillis(TimeUnit.MILLISECONDS.convert(1L, TimeUnit.HOURS))
+                    // When summary atoms are populated once a week, watchdog needs additional time
+                    // for reading from disk/DB.
+                    .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.
@@ -195,11 +233,17 @@
     @GuardedBy("mLock")
     private @GarageMode int mCurrentGarageMode;
     @GuardedBy("mLock")
-    private TimeSourceInterface mTimeSource;
+    private TimeSource mTimeSource;
     @GuardedBy("mLock")
     private long mOveruseHandlingDelayMills;
     @GuardedBy("mLock")
     private long mRecurringOveruseThreshold;
+    @GuardedBy("mLock")
+    private ZonedDateTime mLastSystemIoUsageSummaryReportedDate;
+    @GuardedBy("mLock")
+    private ZonedDateTime mLastUidIoUsageSummaryReportedDate;
+    @GuardedBy("mLock")
+    private int mUidIoUsageSummaryTopCount;
 
     private final ICarUxRestrictionsChangeListener mCarUxRestrictionsChangeListener =
             new ICarUxRestrictionsChangeListener.Stub() {
@@ -213,7 +257,8 @@
             };
 
     public WatchdogPerfHandler(Context context, CarWatchdogDaemonHelper daemonHelper,
-            PackageInfoHandler packageInfoHandler, WatchdogStorage watchdogStorage) {
+            PackageInfoHandler packageInfoHandler, WatchdogStorage watchdogStorage,
+            TimeSource timeSource) {
         mContext = context;
         mCarWatchdogDaemonHelper = daemonHelper;
         mPackageInfoHandler = packageInfoHandler;
@@ -222,11 +267,12 @@
                 CarWatchdogService.class.getSimpleName()).getLooper());
         mWatchdogStorage = watchdogStorage;
         mOveruseConfigurationCache = new OveruseConfigurationCache();
-        mTimeSource = SYSTEM_INSTANCE;
+        mTimeSource = timeSource;
         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;
     }
 
     /** Initializes the handler. */
@@ -237,11 +283,27 @@
             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.
+            StatsManager statsManager = mContext.getSystemService(StatsManager.class);
+            statsManager.setPullAtomCallback(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY,
+                    PULL_ATOM_METADATA, ConcurrentUtils.DIRECT_EXECUTOR, this::onPullAtom);
+            statsManager.setPullAtomCallback(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY,
+                    PULL_ATOM_METADATA, ConcurrentUtils.DIRECT_EXECUTOR, this::onPullAtom);
+        });
 
-        CarLocalServices.getService(CarUxRestrictionsManagerService.class)
-                .registerUxRestrictionsChangeListener(
-                        mCarUxRestrictionsChangeListener, Display.DEFAULT_DISPLAY);
+        CarUxRestrictionsManagerService carUxRestrictionsManagerService =
+                CarLocalServices.getService(CarUxRestrictionsManagerService.class);
+        CarUxRestrictions uxRestrictions =
+                carUxRestrictionsManagerService.getCurrentUxRestrictions();
+        synchronized (mLock) {
+            mCurrentUxRestrictions = uxRestrictions;
+            applyCurrentUxRestrictionsLocked();
+        }
+        carUxRestrictionsManagerService.registerUxRestrictionsChangeListener(
+                mCarUxRestrictionsChangeListener, Display.DEFAULT_DISPLAY);
 
         if (DEBUG) {
             Slogf.d(TAG, "WatchdogPerfHandler is initialized");
@@ -820,13 +882,6 @@
         }
     }
 
-    /** Sets the time source. */
-    public void setTimeSource(TimeSourceInterface timeSource) {
-        synchronized (mLock) {
-            mTimeSource = timeSource;
-        }
-    }
-
     /**
      * Sets the delay to handle resource overuse after the package is notified of resource overuse.
      */
@@ -843,6 +898,56 @@
         }
     }
 
+    /** Sets top N UID I/O usage summaries to report on stats pull. */
+    public void setUidIoUsageSummaryTopCount(int uidIoUsageSummaryTopCount) {
+        synchronized (mLock) {
+            mUidIoUsageSummaryTopCount = uidIoUsageSummaryTopCount;
+        }
+    }
+
+    /** Writes to watchdog metadata file. */
+    public void writeMetadataFile() {
+        ZonedDateTime systemIoUsageSummaryReportDate;
+        ZonedDateTime uidIoUsageSummaryReportDate;
+        synchronized (mLock) {
+            if (mLastSystemIoUsageSummaryReportedDate == null
+                    && mLastUidIoUsageSummaryReportedDate == null) {
+                return;
+            }
+            systemIoUsageSummaryReportDate = mLastSystemIoUsageSummaryReportedDate;
+            uidIoUsageSummaryReportDate = mLastUidIoUsageSummaryReportedDate;
+        }
+        File file = getWatchdogMetadataFile();
+        AtomicFile atomicFile = new AtomicFile(file);
+        FileOutputStream fos = null;
+        try {
+            fos = atomicFile.startWrite();
+            try (JsonWriter jsonWriter =
+                         new JsonWriter(new OutputStreamWriter(fos, StandardCharsets.UTF_8))) {
+                jsonWriter.beginObject();
+                if (systemIoUsageSummaryReportDate != null) {
+                    jsonWriter.name(SYSTEM_IO_USAGE_SUMMARY_REPORTED_DATE)
+                            .value(systemIoUsageSummaryReportDate
+                                    .format(DateTimeFormatter.ISO_DATE_TIME));
+                }
+                if (uidIoUsageSummaryReportDate != null) {
+                    jsonWriter.name(UID_IO_USAGE_SUMMARY_REPORTED_DATE)
+                            .value(uidIoUsageSummaryReportDate
+                                    .format(DateTimeFormatter.ISO_DATE_TIME));
+                }
+                jsonWriter.endObject();
+            }
+            atomicFile.finishWrite(fos);
+            if (DEBUG) {
+                Slogf.e(TAG, "Successfully wrote watchdog metadata file '%s'",
+                        file.getAbsoluteFile());
+            }
+        } catch (IOException e) {
+            Slogf.e(TAG, e, "Failed to write watchdog metadata file '%s'", file.getAbsoluteFile());
+            atomicFile.failWrite(fos);
+        }
+    }
+
     /** Fetches and syncs the resource overuse configurations from watchdog daemon. */
     private void fetchAndSyncResourceOveruseConfigurations() {
         List<android.automotive.watchdog.internal.ResourceOveruseConfiguration> internalConfigs;
@@ -890,8 +995,7 @@
                 usage.setKillableState(entry.killableState);
                 mUsageByUserPackage.put(key, usage);
             }
-            ZonedDateTime curReportDate =
-                    mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+            ZonedDateTime curReportDate = mTimeSource.getCurrentDate();
             for (int i = 0; i < ioStatsEntries.size(); ++i) {
                 WatchdogStorage.IoUsageStatsEntry entry = ioStatsEntries.get(i);
                 String key = getUserPackageUniqueId(entry.userId, entry.packageName);
@@ -1025,8 +1129,7 @@
 
     @GuardedBy("mLock")
     private void checkAndHandleDateChangeLocked() {
-        ZonedDateTime currentDate = mTimeSource.now().atZone(ZoneOffset.UTC)
-                .truncatedTo(STATS_TEMPORAL_UNIT);
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
         if (currentDate.equals(mLatestStatsReportDate)) {
             return;
         }
@@ -1440,6 +1543,269 @@
                         writtenBytes.backgroundBytes, writtenBytes.garageModeBytes));
     }
 
+    private int onPullAtom(int atomTag, List<StatsEvent> data) {
+        if (atomTag != CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY
+                && atomTag != CAR_WATCHDOG_UID_IO_USAGE_SUMMARY) {
+            Slogf.e(TAG, "Unexpected atom tag: %d", atomTag);
+            return PULL_SKIP;
+        }
+        synchronized (mLock) {
+            if (mLastSystemIoUsageSummaryReportedDate == null
+                    || mLastUidIoUsageSummaryReportedDate == null) {
+                readMetadataFileLocked();
+            }
+        }
+        ZonedDateTime reportDate;
+        switch (atomTag) {
+            case CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY:
+                synchronized (mLock) {
+                    reportDate = mLastSystemIoUsageSummaryReportedDate;
+                }
+                pullAtomsForWeeklyPeriodsSinceReportedDate(reportDate, data,
+                        this::pullSystemIoUsageSummaryStatsEvents);
+                synchronized (mLock) {
+                    mLastSystemIoUsageSummaryReportedDate = mTimeSource.getCurrentDate();
+                }
+                break;
+            case CAR_WATCHDOG_UID_IO_USAGE_SUMMARY:
+                synchronized (mLock) {
+                    reportDate = mLastUidIoUsageSummaryReportedDate;
+                }
+                pullAtomsForWeeklyPeriodsSinceReportedDate(reportDate, data,
+                        this::pullUidIoUsageSummaryStatsEvents);
+                synchronized (mLock) {
+                    mLastUidIoUsageSummaryReportedDate = mTimeSource.getCurrentDate();
+                }
+                break;
+        }
+        return PULL_SUCCESS;
+    }
+
+    @GuardedBy("mLock")
+    private void readMetadataFileLocked() {
+        mLastSystemIoUsageSummaryReportedDate = mLastUidIoUsageSummaryReportedDate =
+                mTimeSource.getCurrentDate().minus(RETENTION_PERIOD);
+        File file = getWatchdogMetadataFile();
+        if (!file.exists()) {
+            Slogf.e(TAG, "Watchdog metadata file '%s' doesn't exist", file.getAbsoluteFile());
+            return;
+        }
+        AtomicFile atomicFile = new AtomicFile(file);
+        try (FileInputStream fis = atomicFile.openRead()) {
+            JsonReader reader = new JsonReader(new InputStreamReader(fis, StandardCharsets.UTF_8));
+            reader.beginObject();
+            while (reader.hasNext()) {
+                String name = reader.nextName();
+                switch (name) {
+                    case SYSTEM_IO_USAGE_SUMMARY_REPORTED_DATE:
+                        mLastSystemIoUsageSummaryReportedDate =
+                                ZonedDateTime.parse(reader.nextString(),
+                                        DateTimeFormatter.ISO_DATE_TIME.withZone(ZONE_OFFSET));
+                        break;
+                    case UID_IO_USAGE_SUMMARY_REPORTED_DATE:
+                        mLastUidIoUsageSummaryReportedDate =
+                                ZonedDateTime.parse(reader.nextString(),
+                                        DateTimeFormatter.ISO_DATE_TIME.withZone(ZONE_OFFSET));
+                        break;
+                    default:
+                        Slogf.w(TAG, "Unrecognized key: %s", name);
+                        reader.skipValue();
+                }
+            }
+            reader.endObject();
+            if (DEBUG) {
+                Slogf.e(TAG, "Successfully read watchdog metadata file '%s'",
+                        file.getAbsoluteFile());
+            }
+        } catch (IOException e) {
+            Slogf.e(TAG, e, "Failed to read watchdog metadata file '%s'", file.getAbsoluteFile());
+        } catch (NumberFormatException | IllegalStateException | DateTimeParseException e) {
+            Slogf.e(TAG, e, "Unexpected format in watchdog metadata file '%s'",
+                    file.getAbsoluteFile());
+        }
+    }
+
+    private void pullAtomsForWeeklyPeriodsSinceReportedDate(ZonedDateTime reportedDate,
+            List<StatsEvent> data, BiConsumer<Pair<ZonedDateTime, ZonedDateTime>,
+            List<StatsEvent>> pullAtomCallback) {
+        ZonedDateTime now;
+        synchronized (mLock) {
+            now = mTimeSource.getCurrentDate();
+        }
+        ZonedDateTime nextReportWeekStartDate = reportedDate.with(ChronoField.DAY_OF_WEEK, 1)
+                .truncatedTo(ChronoUnit.DAYS);
+        while (ChronoUnit.WEEKS.between(nextReportWeekStartDate, now) > 0) {
+            pullAtomCallback.accept(
+                    new Pair<>(nextReportWeekStartDate, nextReportWeekStartDate.plusWeeks(1)),
+                    data);
+            nextReportWeekStartDate = nextReportWeekStartDate.plusWeeks(1);
+        }
+    }
+
+    private void pullSystemIoUsageSummaryStatsEvents(Pair<ZonedDateTime, ZonedDateTime> period,
+            List<StatsEvent> data) {
+        List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries =
+                mWatchdogStorage.getDailySystemIoUsageSummaries(period.first.toEpochSecond(),
+                        period.second.toEpochSecond());
+        if (dailyIoUsageSummaries == null) {
+            Slogf.i(TAG, "No system I/O usage summary stats available to pull");
+            return;
+        }
+
+        AtomsProto.CarWatchdogEventTimePeriod evenTimePeriod =
+                AtomsProto.CarWatchdogEventTimePeriod.newBuilder()
+                        .setPeriod(AtomsProto.CarWatchdogEventTimePeriod.Period.WEEKLY)
+                        .setStartTimeMillis(period.first.toEpochSecond()).build();
+        data.add(CarStatsLog.buildStatsEvent(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY,
+                AtomsProto.CarWatchdogIoUsageSummary.newBuilder()
+                        .setEventTimePeriod(evenTimePeriod)
+                        .addAllDailyIoUsageSummary(dailyIoUsageSummaries).build()
+                        .toByteArray()));
+
+        Slogf.i(TAG, "Successfully pulled system I/O usage summary stats");
+    }
+
+    private void pullUidIoUsageSummaryStatsEvents(Pair<ZonedDateTime, ZonedDateTime> period,
+            List<StatsEvent> data) {
+        int numTopUsers;
+        synchronized (mLock) {
+            numTopUsers = mUidIoUsageSummaryTopCount;
+        }
+        // Fetch summaries for twice the top N user packages because if the UID cannot be resolved
+        // for some user packages, the fetched summaries will still contain enough entries to pull.
+        List<WatchdogStorage.UserPackageDailySummaries> topUsersDailyIoUsageSummaries =
+                mWatchdogStorage.getTopUsersDailyIoUsageSummaries(numTopUsers * 2,
+                        UID_IO_USAGE_SUMMARY_MIN_WEEKLY_WRITTEN_BYTES,
+                        period.first.toEpochSecond(), period.second.toEpochSecond());
+        if (topUsersDailyIoUsageSummaries == null) {
+            Slogf.i(TAG, "No top users' I/O usage summary stats available to pull");
+            return;
+        }
+
+        SparseArray<List<String>> genericPackageNamesByUserId = new SparseArray<>();
+        for (int i = 0; i < topUsersDailyIoUsageSummaries.size(); ++i) {
+            WatchdogStorage.UserPackageDailySummaries entry =
+                    topUsersDailyIoUsageSummaries.get(i);
+            List<String> genericPackageNames = genericPackageNamesByUserId.get(entry.userId);
+            if (genericPackageNames == null) {
+                genericPackageNames = new ArrayList<>();
+            }
+            genericPackageNames.add(entry.packageName);
+            genericPackageNamesByUserId.put(entry.userId, genericPackageNames);
+        }
+
+        SparseArray<Map<String, Integer>> packageUidsByUserId =
+                getPackageUidsForUsers(genericPackageNamesByUserId);
+
+        AtomsProto.CarWatchdogEventTimePeriod.Builder evenTimePeriodBuilder =
+                AtomsProto.CarWatchdogEventTimePeriod.newBuilder()
+                        .setPeriod(AtomsProto.CarWatchdogEventTimePeriod.Period.WEEKLY)
+                        .setStartTimeMillis(period.first.toEpochSecond());
+
+        int numPulledUidSummaryStats = 0;
+        for (int i = 0; i < topUsersDailyIoUsageSummaries.size()
+                && numPulledUidSummaryStats < numTopUsers; ++i) {
+            WatchdogStorage.UserPackageDailySummaries entry = topUsersDailyIoUsageSummaries.get(i);
+            Map<String, Integer> uidsByGenericPackageName = packageUidsByUserId.get(entry.userId);
+            if (uidsByGenericPackageName == null
+                    || !uidsByGenericPackageName.containsKey(entry.packageName)) {
+                Slogf.e(TAG, "Failed to fetch uid for package %s and user %d. So, skipping "
+                        + "reporting stats for this user package", entry.packageName, entry.userId);
+                continue;
+            }
+            data.add(CarStatsLog.buildStatsEvent(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY,
+                    uidsByGenericPackageName.get(entry.packageName),
+                    AtomsProto.CarWatchdogIoUsageSummary.newBuilder()
+                            .setEventTimePeriod(evenTimePeriodBuilder)
+                            .addAllDailyIoUsageSummary(entry.dailyIoUsageSummaries).build()
+                            .toByteArray()));
+            ++numPulledUidSummaryStats;
+        }
+
+        Slogf.e(TAG, "Successfully pulled top %d users' I/O usage summary stats",
+                numPulledUidSummaryStats);
+    }
+
+    private SparseArray<Map<String, Integer>> getPackageUidsForUsers(
+            SparseArray<List<String>> genericPackageNamesByUserId) {
+        PackageManager pm = mContext.getPackageManager();
+        SparseArray<Map<String, Integer>> packageUidsByUserId = new SparseArray<>();
+        for (int i = 0; i < genericPackageNamesByUserId.size(); ++i) {
+            int userId = genericPackageNamesByUserId.keyAt(i);
+            Map<String, Integer> uidsByGenericPackageName = getPackageUidsForUser(pm,
+                    genericPackageNamesByUserId.valueAt(i), userId);
+            if (!uidsByGenericPackageName.isEmpty()) {
+                packageUidsByUserId.put(userId, uidsByGenericPackageName);
+            }
+        }
+        return packageUidsByUserId;
+    }
+
+    /**
+     * Returns UIDs for the given generic package names belonging to the given user.
+     *
+     * <p>{@code pm.getInstalledPackagesAsUser} call is expensive as it fetches all installed
+     * packages for the given user. Thus this method should be called for all packages that requires
+     * the UIDs to be resolved in a single call.
+     */
+    private Map<String, Integer> getPackageUidsForUser(PackageManager pm,
+            List<String> genericPackageNames, int userId) {
+        Map<String, Integer> uidsByGenericPackageNames = new ArrayMap<>();
+        Set<String> resolveSharedUserIds = new ArraySet<>();
+        for (int i = 0; i < genericPackageNames.size(); ++i) {
+            String genericPackageName = genericPackageNames.get(i);
+            PackageResourceUsage usage;
+            synchronized (mLock) {
+                usage = mUsageByUserPackage.get(getUserPackageUniqueId(userId,
+                        genericPackageName));
+            }
+            if (usage != null && usage.getUid() != INVALID_UID) {
+                uidsByGenericPackageNames.put(genericPackageName, usage.getUid());
+                continue;
+            }
+            if (genericPackageName.startsWith(SHARED_PACKAGE_PREFIX)) {
+                resolveSharedUserIds.add(
+                        genericPackageName.substring(SHARED_PACKAGE_PREFIX.length()));
+                continue;
+            }
+            int uid = getPackageUidAsUser(pm, genericPackageName, userId);
+            if (uid != INVALID_UID) {
+                uidsByGenericPackageNames.put(genericPackageName, uid);
+            }
+        }
+        if (resolveSharedUserIds.isEmpty()) {
+            return uidsByGenericPackageNames;
+        }
+        List<PackageInfo> packageInfos = pm.getInstalledPackagesAsUser(/* flags= */ 0, userId);
+        for (int i = 0; i < packageInfos.size() && !resolveSharedUserIds.isEmpty(); ++i) {
+            PackageInfo packageInfo = packageInfos.get(i);
+            if (packageInfo.sharedUserId == null
+                    || !resolveSharedUserIds.contains(packageInfo.sharedUserId)) {
+                continue;
+            }
+            int uid = getPackageUidAsUser(pm, packageInfo.packageName, userId);
+            if (uid != INVALID_UID) {
+                uidsByGenericPackageNames.put(SHARED_PACKAGE_PREFIX + packageInfo.sharedUserId,
+                        uid);
+            }
+            resolveSharedUserIds.remove(packageInfo.sharedUserId);
+        }
+        return uidsByGenericPackageNames;
+    }
+
+    private int getPackageUidAsUser(PackageManager pm, String packageName, @UserIdInt int userId) {
+        try {
+            return pm.getPackageUidAsUser(packageName, userId);
+        } catch (PackageManager.NameNotFoundException e) {
+            Slogf.e(TAG, "Package %s for user %d is not found", packageName, userId);
+            return INVALID_UID;
+        }
+    }
+
+    private static File getWatchdogMetadataFile() {
+        return new File(CarWatchdogService.getWatchdogDirFile(), METADATA_FILENAME);
+    }
+
     private static String getUserPackageUniqueId(@UserIdInt int userId, String genericPackageName) {
         return userId + ":" + genericPackageName;
     }
@@ -1798,10 +2164,18 @@
     @VisibleForTesting
     static AtomsProto.CarWatchdogPerStateBytes constructCarWatchdogPerStateBytes(
             long foregroundBytes, long backgroundBytes, long garageModeBytes) {
-        return AtomsProto.CarWatchdogPerStateBytes.newBuilder()
-                .setForegroundBytes(foregroundBytes)
-                .setBackgroundBytes(backgroundBytes)
-                .setGarageModeBytes(garageModeBytes).build();
+        AtomsProto.CarWatchdogPerStateBytes.Builder perStateBytesBuilder =
+                AtomsProto.CarWatchdogPerStateBytes.newBuilder();
+        if (foregroundBytes != 0) {
+            perStateBytesBuilder.setForegroundBytes(foregroundBytes);
+        }
+        if (backgroundBytes != 0) {
+            perStateBytesBuilder.setBackgroundBytes(backgroundBytes);
+        }
+        if (garageModeBytes != 0) {
+            perStateBytesBuilder.setGarageModeBytes(garageModeBytes);
+        }
+        return perStateBytesBuilder.build();
     }
 
     private final class PackageResourceUsage {
diff --git a/service/src/com/android/car/watchdog/WatchdogStorage.java b/service/src/com/android/car/watchdog/WatchdogStorage.java
index 5b0ebb5..be1717c 100644
--- a/service/src/com/android/car/watchdog/WatchdogStorage.java
+++ b/service/src/com/android/car/watchdog/WatchdogStorage.java
@@ -16,7 +16,7 @@
 
 package com.android.car.watchdog;
 
-import static com.android.car.watchdog.CarWatchdogService.SYSTEM_INSTANCE;
+import static com.android.car.watchdog.TimeSource.ZONE_OFFSET;
 
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -40,15 +40,17 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.utils.Slogf;
 
+import java.io.File;
 import java.time.Instant;
 import java.time.Period;
-import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 import java.time.temporal.TemporalUnit;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 
 /**
  * Defines the database to store/retrieve system resource stats history from local storage.
@@ -61,26 +63,27 @@
     /* Number of days to retain the stats in local storage. */
     public static final Period RETENTION_PERIOD =
             Period.ofDays(RETENTION_PERIOD_IN_DAYS).normalized();
-    /* Zone offset for all date based table entries. */
-    public static final ZoneOffset ZONE_OFFSET = ZoneOffset.UTC;
+    public static final String ZONE_MODIFIER = "utc";
+    public static final String DATE_MODIFIER = "unixepoch";
 
     private final WatchdogDbHelper mDbHelper;
     private final ArrayMap<String, UserPackage> mUserPackagesByKey = new ArrayMap<>();
     private final ArrayMap<String, UserPackage> mUserPackagesById = new ArrayMap<>();
-    private TimeSourceInterface mTimeSource = SYSTEM_INSTANCE;
+    private TimeSource mTimeSource;
     private final Object mLock = new Object();
     // Cache of today's I/O overuse stats collected during the previous boot. The data contained in
     // the cache won't change until the next boot, so it is safe to cache the data in memory.
     @GuardedBy("mLock")
     private final List<IoUsageStatsEntry> mTodayIoUsageStatsEntries = new ArrayList<>();
 
-    public WatchdogStorage(Context context) {
-        this(context, /* useDataSystemCarDir= */ true);
+    public WatchdogStorage(Context context, TimeSource timeSource) {
+        this(context, /* useDataSystemCarDir= */ true, timeSource);
     }
 
     @VisibleForTesting
-    WatchdogStorage(Context context, boolean useDataSystemCarDir) {
-        mDbHelper = new WatchdogDbHelper(context, useDataSystemCarDir);
+    WatchdogStorage(Context context, boolean useDataSystemCarDir, TimeSource timeSource) {
+        mTimeSource = timeSource;
+        mDbHelper = new WatchdogDbHelper(context, useDataSystemCarDir, mTimeSource);
     }
 
     /** Releases resources. */
@@ -146,13 +149,12 @@
             if (!mTodayIoUsageStatsEntries.isEmpty()) {
                 return new ArrayList<>(mTodayIoUsageStatsEntries);
             }
-            ZonedDateTime statsDate =
-                    mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
-            long startEpochSeconds = statsDate.toEpochSecond();
-            long endEpochSeconds = mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond();
+            long includingStartEpochSeconds = mTimeSource.getCurrentDate().toEpochSecond();
+            long excludingEndEpochSeconds = mTimeSource.getCurrentDateTime().toEpochSecond();
             ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsagesById;
             try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
-                ioUsagesById = IoUsageStatsTable.queryStats(db, startEpochSeconds, endEpochSeconds);
+                ioUsagesById = IoUsageStatsTable.queryStats(db, includingStartEpochSeconds,
+                        excludingEndEpochSeconds);
             }
             for (int i = 0; i < ioUsagesById.size(); ++i) {
                 String id = ioUsagesById.keyAt(i);
@@ -191,12 +193,11 @@
      * {@code null} when stats are not available.
      */
     @Nullable
-    public IoOveruseStats getHistoricalIoOveruseStats(
-            @UserIdInt int userId, String packageName, int numDaysAgo) {
-        ZonedDateTime currentDate =
-                mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
-        long startEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond();
-        long endEpochSeconds = currentDate.toEpochSecond();
+    public IoOveruseStats getHistoricalIoOveruseStats(@UserIdInt int userId, String packageName,
+            int numDaysAgo) {
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
+        long includingStartEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond();
+        long excludingEndEpochSeconds = currentDate.toEpochSecond();
         try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
             UserPackage userPackage = mUserPackagesByKey.get(
                     UserPackage.getKey(userId, packageName));
@@ -204,12 +205,59 @@
                 /* Packages without historical stats don't have userPackage entry. */
                 return null;
             }
-            return IoUsageStatsTable.queryHistoricalStats(
-                    db, userPackage.getUniqueId(), startEpochSeconds, endEpochSeconds);
+            return IoUsageStatsTable.queryIoOveruseStatsForUniqueId(db, userPackage.getUniqueId(),
+                    includingStartEpochSeconds, excludingEndEpochSeconds);
         }
     }
 
     /**
+     * Returns daily system-level I/O usage summaries for the given period or {@code null} when
+     * summaries are not available.
+     */
+    public @Nullable List<AtomsProto.CarWatchdogDailyIoUsageSummary> getDailySystemIoUsageSummaries(
+            long includingStartEpochSeconds, long excludingEndEpochSeconds) {
+        try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+            return IoUsageStatsTable.queryDailySystemIoUsageSummaries(db,
+                    includingStartEpochSeconds, excludingEndEpochSeconds);
+        }
+    }
+
+    /**
+     * Returns top N disk I/O users' daily I/O usage summaries for the given period or {@code null}
+     * when summaries are not available.
+     */
+    public @Nullable List<UserPackageDailySummaries> getTopUsersDailyIoUsageSummaries(
+            int numTopUsers, long minTotalWrittenBytes, long includingStartEpochSeconds,
+            long excludingEndEpochSeconds) {
+        ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>> summariesById;
+        try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+            summariesById = IoUsageStatsTable.queryTopUsersDailyIoUsageSummaries(db,
+                    numTopUsers, minTotalWrittenBytes, includingStartEpochSeconds,
+                    excludingEndEpochSeconds);
+        }
+        if (summariesById == null) {
+            return null;
+        }
+        ArrayList<UserPackageDailySummaries> userPackageDailySummaries = new ArrayList<>();
+        for (int i = 0; i < summariesById.size(); ++i) {
+            String id = summariesById.keyAt(i);
+            UserPackage userPackage = mUserPackagesById.get(id);
+            if (userPackage == null) {
+                Slogf.i(TAG,
+                        "Failed to find user id and package name for unique database id: '%s'",
+                        id);
+                continue;
+            }
+            userPackageDailySummaries.add(new UserPackageDailySummaries(userPackage.getUserId(),
+                    userPackage.getPackageName(), summariesById.valueAt(i)));
+        }
+        userPackageDailySummaries
+                .sort(Comparator.comparingLong(UserPackageDailySummaries::getTotalWrittenBytes)
+                        .reversed());
+        return userPackageDailySummaries;
+    }
+
+    /**
      * Deletes all user package settings and resource stats for all non-alive users.
      *
      * @param aliveUserIds Array of alive user ids.
@@ -230,8 +278,7 @@
 
     @VisibleForTesting
     boolean saveIoUsageStats(List<IoUsageStatsEntry> entries, boolean shouldCheckRetention) {
-        ZonedDateTime currentDate =
-                mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
         List<ContentValues> rows = new ArrayList<>(entries.size());
         for (int i = 0; i < entries.size(); ++i) {
             IoUsageStatsEntry entry = entries.get(i);
@@ -259,12 +306,6 @@
         }
     }
 
-    @VisibleForTesting
-    void setTimeSource(TimeSourceInterface timeSource) {
-        mTimeSource = timeSource;
-        mDbHelper.setTimeSource(timeSource);
-    }
-
     private void populateUserPackages(SQLiteDatabase db, ArraySet<Integer> users) {
         List<UserPackage> userPackages = UserPackageSettingsTable.queryUserPackages(db, users);
         for (int i = 0; i < userPackages.size(); ++i) {
@@ -302,16 +343,106 @@
 
     /** Defines the user package settings entry stored in the UserPackageSettingsTable. */
     static final class UserPackageSettingsEntry {
-        public final String packageName;
         public final @UserIdInt int userId;
+        public final String packageName;
         public final @KillableState int killableState;
 
-        UserPackageSettingsEntry(
-                @UserIdInt int userId, String packageName, @KillableState int killableState) {
+        UserPackageSettingsEntry(@UserIdInt int userId, String packageName,
+                @KillableState int killableState) {
             this.userId = userId;
             this.packageName = packageName;
             this.killableState = killableState;
         }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            if (!(obj instanceof UserPackageSettingsEntry)) {
+                return false;
+            }
+            UserPackageSettingsEntry other = (UserPackageSettingsEntry) obj;
+            return userId == other.userId && packageName.equals(other.packageName)
+                    && killableState == other.killableState;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(userId, packageName, killableState);
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder().append("UserPackageSettingsEntry{userId: ").append(userId)
+                    .append(", packageName: ").append(packageName)
+                    .append(", killableState: ").append(killableState).append('}')
+                    .toString();
+        }
+    }
+
+    /** Defines the daily summaries for user packages. */
+    static final class UserPackageDailySummaries {
+        public final @UserIdInt int userId;
+        public final String packageName;
+        public final List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries;
+        private final long mTotalWrittenBytes;
+
+        UserPackageDailySummaries(@UserIdInt int userId, String packageName,
+                List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailyIoUsageSummaries) {
+            this.userId = userId;
+            this.packageName = packageName;
+            this.dailyIoUsageSummaries = dailyIoUsageSummaries;
+            this.mTotalWrittenBytes = computeTotalWrittenBytes();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            if (!(obj instanceof UserPackageDailySummaries)) {
+                return false;
+            }
+            UserPackageDailySummaries other = (UserPackageDailySummaries) obj;
+            return userId == other.userId && packageName.equals(other.packageName)
+                    && dailyIoUsageSummaries.equals(other.dailyIoUsageSummaries);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(userId, packageName, dailyIoUsageSummaries, mTotalWrittenBytes);
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder().append("UserPackageDailySummaries{userId: ").append(userId)
+                    .append(", packageName: ").append(packageName)
+                    .append(", dailyIoUsageSummaries: ").append(dailyIoUsageSummaries).append('}')
+                    .toString();
+        }
+
+        long getTotalWrittenBytes() {
+            return mTotalWrittenBytes;
+        }
+
+        long computeTotalWrittenBytes() {
+            long totalBytes = 0;
+            for (int i = 0; i < dailyIoUsageSummaries.size(); ++i) {
+                AtomsProto.CarWatchdogPerStateBytes writtenBytes =
+                        dailyIoUsageSummaries.get(i).getWrittenBytes();
+                if (writtenBytes.hasForegroundBytes()) {
+                    totalBytes += writtenBytes.getForegroundBytes();
+                }
+                if (writtenBytes.hasBackgroundBytes()) {
+                    totalBytes += writtenBytes.getBackgroundBytes();
+                }
+                if (writtenBytes.hasGarageModeBytes()) {
+                    totalBytes += writtenBytes.getGarageModeBytes();
+                }
+            }
+            return totalBytes;
+        }
     }
 
     /**
@@ -523,7 +654,7 @@
         }
 
         public static ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> queryStats(
-                SQLiteDatabase db, long startEpochSeconds, long endEpochSeconds) {
+                SQLiteDatabase db, long includingStartEpochSeconds, long excludingEndEpochSeconds) {
             StringBuilder queryBuilder = new StringBuilder();
             queryBuilder.append("SELECT ")
                     .append(COLUMN_USER_PACKAGE_ID).append(", ")
@@ -543,8 +674,8 @@
                     .append(COLUMN_DATE_EPOCH).append(">= ? and ")
                     .append(COLUMN_DATE_EPOCH).append("< ? GROUP BY ")
                     .append(COLUMN_USER_PACKAGE_ID);
-            String[] selectionArgs = new String[]{
-                    String.valueOf(startEpochSeconds), String.valueOf(endEpochSeconds)};
+            String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds),
+                    String.valueOf(excludingEndEpochSeconds)};
 
             ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsageById = new ArrayMap<>();
             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
@@ -552,7 +683,8 @@
                     android.automotive.watchdog.IoOveruseStats ioOveruseStats =
                             new android.automotive.watchdog.IoOveruseStats();
                     ioOveruseStats.startTime = cursor.getLong(1);
-                    ioOveruseStats.durationInSeconds = endEpochSeconds - startEpochSeconds;
+                    ioOveruseStats.durationInSeconds =
+                            excludingEndEpochSeconds - includingStartEpochSeconds;
                     ioOveruseStats.totalOveruses = cursor.getInt(2);
                     ioOveruseStats.writtenBytes = new PerStateBytes();
                     ioOveruseStats.writtenBytes.foregroundBytes = cursor.getLong(4);
@@ -574,8 +706,8 @@
             return ioUsageById;
         }
 
-        public static IoOveruseStats queryHistoricalStats(SQLiteDatabase db, String uniqueId,
-                long startEpochSeconds, long endEpochSeconds) {
+        public static @Nullable IoOveruseStats queryIoOveruseStatsForUniqueId(SQLiteDatabase db,
+                String uniqueId, long includingStartEpochSeconds, long excludingEndEpochSeconds) {
             StringBuilder queryBuilder = new StringBuilder();
             queryBuilder.append("SELECT SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
                     .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ")
@@ -588,11 +720,12 @@
                     .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
                     .append(COLUMN_DATE_EPOCH).append("< ?");
             String[] selectionArgs = new String[]{uniqueId,
-                    String.valueOf(startEpochSeconds), String.valueOf(endEpochSeconds)};
+                    String.valueOf(includingStartEpochSeconds),
+                    String.valueOf(excludingEndEpochSeconds)};
             long totalOveruses = 0;
             long totalTimesKilled = 0;
             long totalBytesWritten = 0;
-            long earliestEpochSecond = endEpochSeconds;
+            long earliestEpochSecond = excludingEndEpochSeconds;
             try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
                 if (cursor.getCount() == 0) {
                     return null;
@@ -607,7 +740,7 @@
             if (totalBytesWritten == 0) {
                 return null;
             }
-            long durationInSeconds = endEpochSeconds - earliestEpochSecond;
+            long durationInSeconds = excludingEndEpochSeconds - earliestEpochSecond;
             IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder(
                     earliestEpochSecond, durationInSeconds);
             statsBuilder.setTotalOveruses(totalOveruses);
@@ -616,6 +749,118 @@
             return statsBuilder.build();
         }
 
+        public static @Nullable List<AtomsProto.CarWatchdogDailyIoUsageSummary>
+                queryDailySystemIoUsageSummaries(SQLiteDatabase db, long includingStartEpochSeconds,
+                long excludingEndEpochSeconds) {
+            StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.append("SELECT SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
+                    .append("date(").append(COLUMN_DATE_EPOCH).append(", '").append(DATE_MODIFIER)
+                    .append("', '").append(ZONE_MODIFIER).append("') as stats_date_epoch ")
+                    .append("FROM ").append(TABLE_NAME).append(" WHERE ")
+                    .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
+                    .append(COLUMN_DATE_EPOCH).append(" < ? ")
+                    .append("GROUP BY stats_date_epoch ")
+                    .append("HAVING SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" + ")
+                    .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" + ")
+                    .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(") > 0 ")
+                    .append("ORDER BY stats_date_epoch ASC");
+
+            Slogf.e(TAG, "Query: %s", queryBuilder.toString());
+
+            String[] selectionArgs = new String[]{String.valueOf(includingStartEpochSeconds),
+                    String.valueOf(excludingEndEpochSeconds)};
+            List<AtomsProto.CarWatchdogDailyIoUsageSummary> summaries = new ArrayList<>();
+            try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
+                if (cursor.getCount() == 0) {
+                    return null;
+                }
+                while (cursor.moveToNext()) {
+                    summaries.add(AtomsProto.CarWatchdogDailyIoUsageSummary.newBuilder()
+                            .setWrittenBytes(WatchdogPerfHandler.constructCarWatchdogPerStateBytes(
+                                    /* foregroundBytes= */ cursor.getLong(1),
+                                    /* backgroundBytes= */ cursor.getLong(2),
+                                    /* garageModeBytes= */ cursor.getLong(3)))
+                            .setOveruseCount(cursor.getInt(0))
+                            .build());
+                }
+            }
+            return summaries;
+        }
+
+        public static @Nullable ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>>
+                queryTopUsersDailyIoUsageSummaries(SQLiteDatabase db, int numTopUsers,
+                long minTotalWrittenBytes, long includingStartEpochSeconds,
+                long excludingEndEpochSeconds) {
+            StringBuilder innerQueryBuilder = new StringBuilder();
+            innerQueryBuilder.append("SELECT ").append(COLUMN_USER_PACKAGE_ID)
+                    .append(" FROM (SELECT ").append(COLUMN_USER_PACKAGE_ID).append(", ")
+                    .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" + ")
+                    .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" + ")
+                    .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(") AS total_written_bytes ")
+                    .append("FROM ").append(TABLE_NAME).append(" WHERE ")
+                    .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
+                    .append(COLUMN_DATE_EPOCH).append(" < ?")
+                    .append(" GROUP BY ").append(COLUMN_USER_PACKAGE_ID)
+                    .append(" HAVING total_written_bytes >= ").append(minTotalWrittenBytes)
+                    .append(" ORDER BY total_written_bytes LIMIT ").append(numTopUsers).append(')');
+
+            StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.append("SELECT ").append(COLUMN_USER_PACKAGE_ID).append(", ")
+                    .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
+                    .append("date(").append(COLUMN_DATE_EPOCH).append(", '").append(DATE_MODIFIER)
+                    .append("', '").append(ZONE_MODIFIER).append("') as stats_date_epoch ")
+                    .append("FROM ").append(TABLE_NAME).append(" WHERE ")
+                    .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
+                    .append(COLUMN_DATE_EPOCH).append(" < ? and (")
+                    .append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" > 0 or ")
+                    .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" > 0 or ")
+                    .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(" > 0) and ")
+                    .append(COLUMN_USER_PACKAGE_ID)
+                    .append(" in (").append(innerQueryBuilder)
+                    .append(") GROUP BY stats_date_epoch, ").append(COLUMN_USER_PACKAGE_ID)
+                    .append(" ORDER BY ").append(COLUMN_USER_PACKAGE_ID)
+                    .append(", stats_date_epoch ASC");
+
+            String[] selectionArgs = new String[]{
+                    // Outer query selection arguments.
+                    String.valueOf(includingStartEpochSeconds),
+                    String.valueOf(excludingEndEpochSeconds),
+                    // Inner query selection arguments.
+                    String.valueOf(includingStartEpochSeconds),
+                    String.valueOf(excludingEndEpochSeconds)};
+
+            ArrayMap<String, List<AtomsProto.CarWatchdogDailyIoUsageSummary>> summariesById =
+                    new ArrayMap<>();
+            try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
+                if (cursor.getCount() == 0) {
+                    return null;
+                }
+                while (cursor.moveToNext()) {
+                    String id = cursor.getString(0);
+                    List<AtomsProto.CarWatchdogDailyIoUsageSummary> summaries =
+                            summariesById.get(id);
+                    if (summaries == null) {
+                        summaries = new ArrayList<>();
+                    }
+                    summaries.add(AtomsProto.CarWatchdogDailyIoUsageSummary.newBuilder()
+                            .setWrittenBytes(WatchdogPerfHandler.constructCarWatchdogPerStateBytes(
+                                    /* foregroundBytes= */ cursor.getLong(2),
+                                    /* backgroundBytes= */ cursor.getLong(3),
+                                    /* garageModeBytes= */ cursor.getLong(4)))
+                            .setOveruseCount(cursor.getInt(1))
+                            .build());
+                    summariesById.put(id, summaries);
+                }
+            }
+            return summariesById;
+        }
+
         public static void truncateToDate(SQLiteDatabase db, ZonedDateTime latestTruncateDate) {
             String selection = COLUMN_DATE_EPOCH + " <= ?";
             String[] selectionArgs = { String.valueOf(latestTruncateDate.toEpochSecond()) };
@@ -645,21 +890,23 @@
      * Defines the Watchdog database and database level operations.
      */
     static final class WatchdogDbHelper extends SQLiteOpenHelper {
-        public static final String DATABASE_DIR = "/data/system/car/watchdog/";
         public static final String DATABASE_NAME = "car_watchdog.db";
 
         private static final int DATABASE_VERSION = 1;
 
         private ZonedDateTime mLatestShrinkDate;
-        private TimeSourceInterface mTimeSource = SYSTEM_INSTANCE;
+        private TimeSource mTimeSource;
 
-        WatchdogDbHelper(Context context, boolean useDataSystemCarDir) {
+        WatchdogDbHelper(Context context, boolean useDataSystemCarDir, TimeSource timeSource) {
             /* Use device protected storage because CarService may need to access the database
              * before the user has authenticated.
              */
-            super(context.createDeviceProtectedStorageContext(),
-                    useDataSystemCarDir ? DATABASE_DIR + DATABASE_NAME : DATABASE_NAME,
+            super(context.createDeviceProtectedStorageContext(), useDataSystemCarDir
+                            ? new File(CarWatchdogService.getWatchdogDirFile(), DATABASE_NAME)
+                                    .getAbsolutePath()
+                            : DATABASE_NAME,
                     /* name= */ null, DATABASE_VERSION);
+            mTimeSource = timeSource;
         }
 
         @Override
@@ -675,8 +922,7 @@
         }
 
         public void onShrink(SQLiteDatabase db) {
-            ZonedDateTime currentDate =
-                    mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(ChronoUnit.DAYS);
+            ZonedDateTime currentDate = mTimeSource.getCurrentDate();
             if (currentDate.equals(mLatestShrinkDate)) {
                 return;
             }
@@ -690,10 +936,6 @@
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
             /* Still on the 1st version so no upgrade required. */
         }
-
-        void setTimeSource(TimeSourceInterface timeSource) {
-            mTimeSource = timeSource;
-        }
     }
 
     private static final class UserPackage {
diff --git a/service/src/com/android/car/watchdog/proto/atoms.proto b/service/src/com/android/car/watchdog/proto/atoms.proto
index 95f6210..2f95757 100644
--- a/service/src/com/android/car/watchdog/proto/atoms.proto
+++ b/service/src/com/android/car/watchdog/proto/atoms.proto
@@ -158,3 +158,96 @@
   // Resident set size in kb.
   optional uint64 vm_rss_kb = 7;
 }
+
+/**
+ * Logs total I/O usage summary for all applications and services running in the system.
+ *
+ * Keep in sync with proto file at
+ * packages/services/Car/service/src/com/android/car/watchdog/proto/atoms.proto
+ *
+ * Pulled from:
+ *  packages/services/Car/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+ */
+message CarWatchdogSystemIoUsageSummary {
+  // I/O usage summary for the system.
+  optional CarWatchdogIoUsageSummary io_usage_summary = 1;
+}
+
+/**
+ * Logs I/O usage summary for an UID.
+ *
+ * Keep in sync with proto file at
+ * packages/services/Car/service/src/com/android/car/watchdog/proto/atoms.proto
+ *
+ * Pulled from:
+ *  packages/services/Car/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+ */
+message CarWatchdogUidIoUsageSummary {
+  // UID of the application/service whose usage summary are recorded.
+  optional int32 uid = 1;
+
+  // I/O usage summary for the UID.
+  optional CarWatchdogIoUsageSummary io_usage_summary = 2;
+}
+
+/**
+ * Logs I/O usage summary for a time period.
+ *
+ * Keep in sync with proto file at
+ * packages/services/Car/service/src/com/android/car/watchdog/proto/atoms.proto
+ *
+ * Pulled from:
+ *  packages/services/Car/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+ */
+message CarWatchdogIoUsageSummary {
+  // Summary event time period.
+  optional CarWatchdogEventTimePeriod event_time_period = 1;
+
+  // Daily I/O usage summary for the period. Logs summary entries only for days with I/O usage.
+  // The entries are ordered beginning from the event_time_period.start_time_millis.
+  repeated CarWatchdogDailyIoUsageSummary daily_io_usage_summary = 2;
+}
+
+/**
+ * Logs a car watchdog event's time period.
+ *
+ * Keep in sync with proto file at
+ * packages/services/Car/service/src/com/android/car/watchdog/proto/atoms.proto
+ *
+ * Pulled from:
+ *  packages/services/Car/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+ */
+message CarWatchdogEventTimePeriod {
+  enum Period {
+    UNKNOWN_PERIOD = 0;
+    WEEKLY = 1;
+    BIWEEKLY = 2;
+    MONTHLY = 3;
+  }
+
+  // Start time of the event in milliseconds since epoch.
+  optional uint64 start_time_millis = 1;
+
+  // Period for the event.
+  optional Period period = 2;
+}
+
+/**
+ * Logs daily I/O usage summary.
+ *
+ * Keep in sync with proto file at
+ * packages/services/Car/service/src/com/android/car/watchdog/proto/atoms.proto
+ *
+ * Pulled from:
+ *  packages/services/Car/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+ */
+message CarWatchdogDailyIoUsageSummary {
+  // Total bytes written to disk during a day.
+  optional CarWatchdogPerStateBytes written_bytes = 1;
+
+  // Total uptime for the system or service or application during a day.
+  optional uint64 uptime_millis = 2;
+
+  // Total disk I/O overuses during a day.
+  optional int32 overuse_count = 3;
+}
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 9a24a7e..d607522 100644
--- a/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
@@ -32,6 +32,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.StatsManager;
 import android.automotive.watchdog.internal.ICarWatchdog;
 import android.automotive.watchdog.internal.ICarWatchdogServiceForSystem;
 import android.car.Car;
@@ -50,6 +51,11 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.car.CarLocalServices;
+import com.android.car.CarUxRestrictionsManagerService;
+import com.android.car.power.CarPowerManagementService;
+import com.android.car.systeminterface.SystemInterface;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -57,6 +63,8 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.concurrent.CountDownLatch;
@@ -76,33 +84,48 @@
     private final Handler mMainHandler = new Handler(Looper.getMainLooper());
     private final Executor mExecutor =
             InstrumentationRegistry.getInstrumentation().getTargetContext().getMainExecutor();
+    private final TestTimeSource mTimeSource = new TestTimeSource();
     private final UserInfo[] mUserInfos = new UserInfo[] {
             new UserInfoBuilder(100).setName("user 1").build(),
             new UserInfoBuilder(101).setName("user 2").build()
     };
 
     @Mock private Context mMockContext;
-    @Mock private Car mCar;
-    @Mock private UserManager mUserManager;
-    @Mock private IBinder mDaemonBinder;
-    @Mock private IBinder mServiceBinder;
-    @Mock private ICarWatchdog mCarWatchdogDaemon;
+    @Mock private Car mMockCar;
+    @Mock private UserManager mMockUserManager;
+    @Mock private StatsManager mMockStatsManager;
+    @Mock private SystemInterface mMockSystemInterface;
+    @Mock private CarUxRestrictionsManagerService mMockCarUxRestrictionsManagerService;
+    @Mock private CarPowerManagementService mMockCarPowerManagementService;
+    @Mock private IBinder mMockDaemonBinder;
+    @Mock private IBinder mMockServiceBinder;
+    @Mock private ICarWatchdog mMockCarWatchdogDaemon;
     @Mock private WatchdogStorage mMockWatchdogStorage;
 
     private CarWatchdogService mCarWatchdogService;
     private ICarWatchdogServiceForSystem mWatchdogServiceForSystemImpl;
 
     @Before
-    public void setUpMocks() throws Exception {
-        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage);
+    public void setUp() throws Exception {
+        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage,
+                mTimeSource);
 
-        mockQueryService(CAR_WATCHDOG_DAEMON_INTERFACE, mDaemonBinder, mCarWatchdogDaemon);
-        when(mCar.getEventHandler()).thenReturn(mMainHandler);
-        when(mServiceBinder.queryLocalInterface(anyString())).thenReturn(mCarWatchdogService);
-        when(mMockContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
-        mockUmGetAllUsers(mUserManager, mUserInfos);
-        mockUmIsUserRunning(mUserManager, 100, true);
-        mockUmIsUserRunning(mUserManager, 101, false);
+        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);
+
+        doReturn(mMockSystemInterface)
+                .when(() -> CarLocalServices.getService(SystemInterface.class));
+        doReturn(mMockCarUxRestrictionsManagerService)
+                .when(() -> CarLocalServices.getService(CarUxRestrictionsManagerService.class));
+        doReturn(mMockCarPowerManagementService)
+                .when(() -> CarLocalServices.getService(CarPowerManagementService.class));
+
+        mockUmGetAllUsers(mMockUserManager, mUserInfos);
+        mockUmIsUserRunning(mMockUserManager, 100, true);
+        mockUmIsUserRunning(mMockUserManager, 101, false);
 
         mCarWatchdogService.init();
         mWatchdogServiceForSystemImpl = registerCarWatchdogService();
@@ -111,6 +134,7 @@
     @Override
     protected void onSessionBuilder(CustomMockitoSessionBuilder builder) {
         builder
+            .spyStatic(CarLocalServices.class)
             .spyStatic(ServiceManager.class)
             .spyStatic(UserHandle.class);
     }
@@ -148,12 +172,14 @@
         client.registerClient();
         mWatchdogServiceForSystemImpl.checkIfAlive(123456, TIMEOUT_CRITICAL);
         ArgumentCaptor<int[]> notRespondingClients = ArgumentCaptor.forClass(int[].class);
-        verify(mCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).tellCarWatchdogServiceAlive(
-                eq(mWatchdogServiceForSystemImpl), notRespondingClients.capture(), eq(123456));
+        verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS))
+                .tellCarWatchdogServiceAlive(eq(mWatchdogServiceForSystemImpl),
+                        notRespondingClients.capture(), eq(123456));
         assertThat(notRespondingClients.getValue().length).isEqualTo(0);
         mWatchdogServiceForSystemImpl.checkIfAlive(987654, TIMEOUT_CRITICAL);
-        verify(mCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).tellCarWatchdogServiceAlive(
-                eq(mWatchdogServiceForSystemImpl), notRespondingClients.capture(), eq(987654));
+        verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS))
+                .tellCarWatchdogServiceAlive(eq(mWatchdogServiceForSystemImpl),
+                        notRespondingClients.capture(), eq(987654));
         assertThat(notRespondingClients.getValue().length).isEqualTo(0);
     }
 
@@ -175,13 +201,15 @@
         for (int i = 0; i < clients.size(); i++) {
             assertThat(clients.get(i).mAndroidClient.makeSureHealthCheckDone()).isEqualTo(true);
         }
-        verify(mCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).tellCarWatchdogServiceAlive(
-                eq(mWatchdogServiceForSystemImpl), pidsCaptor.capture(), eq(123456));
+        verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS))
+                .tellCarWatchdogServiceAlive(eq(mWatchdogServiceForSystemImpl),
+                        pidsCaptor.capture(), eq(123456));
         assertThat(pidsCaptor.getValue().length).isEqualTo(0);
 
         mWatchdogServiceForSystemImpl.checkIfAlive(987654, TIMEOUT_CRITICAL);
-        verify(mCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).tellCarWatchdogServiceAlive(
-                eq(mWatchdogServiceForSystemImpl), pidsCaptor.capture(), eq(987654));
+        verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS))
+                .tellCarWatchdogServiceAlive(eq(mWatchdogServiceForSystemImpl),
+                        pidsCaptor.capture(), eq(987654));
         assertThat(pidsCaptor.getValue().length).isEqualTo(2);
     }
 
@@ -190,8 +218,8 @@
                 ArgumentCaptor.forClass(ICarWatchdogServiceForSystem.class);
         // Registering to daemon is done through a message handler. So, a buffer time of 1000ms is
         // given.
-        verify(mCarWatchdogDaemon, timeout(1000)).registerCarWatchdogService(
-                watchdogServiceForSystemImplCaptor.capture());
+        verify(mMockCarWatchdogDaemon, timeout(1000))
+                .registerCarWatchdogService(watchdogServiceForSystemImplCaptor.capture());
         return watchdogServiceForSystemImplCaptor.getValue();
     }
 
@@ -202,8 +230,9 @@
         client.registerClient();
         mWatchdogServiceForSystemImpl.checkIfAlive(123456, TIMEOUT_CRITICAL);
         ArgumentCaptor<int[]> notRespondingClients = ArgumentCaptor.forClass(int[].class);
-        verify(mCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).tellCarWatchdogServiceAlive(
-                eq(mWatchdogServiceForSystemImpl), notRespondingClients.capture(), eq(123456));
+        verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS))
+                .tellCarWatchdogServiceAlive(eq(mWatchdogServiceForSystemImpl),
+                        notRespondingClients.capture(), eq(123456));
         // Checking Android client health is asynchronous, so wait at most 1 second.
         int repeat = 10;
         while (repeat > 0) {
@@ -218,8 +247,9 @@
         assertThat(notRespondingClients.getValue().length).isEqualTo(0);
         assertThat(androidClient.makeSureHealthCheckDone()).isEqualTo(true);
         mWatchdogServiceForSystemImpl.checkIfAlive(987654, TIMEOUT_CRITICAL);
-        verify(mCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).tellCarWatchdogServiceAlive(
-                eq(mWatchdogServiceForSystemImpl), notRespondingClients.capture(), eq(987654));
+        verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS))
+                .tellCarWatchdogServiceAlive(eq(mWatchdogServiceForSystemImpl),
+                        notRespondingClients.capture(), eq(987654));
         assertThat(notRespondingClients.getValue().length).isEqualTo(badClientCount);
     }
 
@@ -236,7 +266,7 @@
         BaseAndroidClient mAndroidClient;
 
         TestClient(BaseAndroidClient actualClient) {
-            mCarWatchdogManager = new CarWatchdogManager(mCar, mServiceBinder);
+            mCarWatchdogManager = new CarWatchdogManager(mMockCar, mMockServiceBinder);
             mAndroidClient = actualClient;
             actualClient.setManager(mCarWatchdogManager);
         }
@@ -321,4 +351,27 @@
             return false;
         }
     }
+
+    private static final class TestTimeSource extends TimeSource {
+        private static final Instant TEST_DATE_TIME = Instant.parse("2021-11-12T13:14:15.16Z");
+        private Instant mNow;
+        TestTimeSource() {
+            mNow = TEST_DATE_TIME;
+        }
+
+        @Override
+        public Instant now() {
+            /* Return the same time, so the tests are deterministic. */
+            return mNow;
+        }
+
+        @Override
+        public String toString() {
+            return "Mocked date to " + now();
+        }
+
+        void updateNow(int numDaysAgo) {
+            mNow = TEST_DATE_TIME.minus(numDaysAgo, ChronoUnit.DAYS);
+        }
+    }
 }
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 cb3eb52..a5d6758 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
@@ -16,6 +16,8 @@
 
 package com.android.car.watchdog;
 
+import static android.app.StatsManager.PULL_SKIP;
+import static android.app.StatsManager.PULL_SUCCESS;
 import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetAllUsers;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUserHandles;
@@ -28,7 +30,11 @@
 import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__GARAGE_MODE;
 import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE;
 import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__UID_STATE__UNKNOWN_UID_STATE;
-import static com.android.car.watchdog.WatchdogStorage.ZONE_OFFSET;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_UID_IO_USAGE_SUMMARY;
+import static com.android.car.watchdog.TimeSource.ZONE_OFFSET;
+import static com.android.car.watchdog.WatchdogPerfHandler.UID_IO_USAGE_SUMMARY_MIN_WEEKLY_WRITTEN_BYTES;
+import static com.android.car.watchdog.WatchdogStorage.RETENTION_PERIOD;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -39,6 +45,7 @@
 import static org.junit.Assert.assertThrows;
 import static org.mockito.AdditionalMatchers.or;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyList;
@@ -54,6 +61,9 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityThread;
+import android.app.StatsManager;
+import android.app.StatsManager.PullAtomMetadata;
+import android.app.StatsManager.StatsPullAtomCallback;
 import android.automotive.watchdog.internal.ApplicationCategoryType;
 import android.automotive.watchdog.internal.ComponentType;
 import android.automotive.watchdog.internal.GarageMode;
@@ -97,6 +107,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.os.Binder;
+import android.os.FileUtils;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -108,6 +119,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
+import android.util.StatsEvent;
 import android.view.Display;
 
 import com.android.car.CarLocalServices;
@@ -115,6 +127,7 @@
 import com.android.car.CarStatsLog;
 import com.android.car.CarUxRestrictionsManagerService;
 import com.android.car.power.CarPowerManagementService;
+import com.android.car.systeminterface.SystemInterface;
 
 import com.google.common.truth.Correspondence;
 
@@ -127,16 +140,21 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.io.File;
+import java.nio.file.Files;
 import java.time.Instant;
 import java.time.ZonedDateTime;
+import java.time.temporal.ChronoField;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BiConsumer;
@@ -151,18 +169,21 @@
     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 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;
 
     @Mock private Context mMockContext;
     @Mock private PackageManager mMockPackageManager;
+    @Mock private StatsManager mMockStatsManager;
     @Mock private UserManager mMockUserManager;
+    @Mock private SystemInterface mMockSystemInterface;
     @Mock private CarPowerManagementService mMockCarPowerManagementService;
     @Mock private CarUxRestrictionsManagerService mMockCarUxRestrictionsManagerService;
     @Mock private IBinder mMockBinder;
     @Mock private ICarWatchdog mMockCarWatchdogDaemon;
     @Mock private WatchdogStorage mMockWatchdogStorage;
 
-
     @Captor private ArgumentCaptor<ICarPowerStateListener> mICarPowerStateListenerCaptor;
     @Captor private ArgumentCaptor<ICarPowerPolicyListener> mICarPowerPolicyListenerCaptor;
     @Captor private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
@@ -174,6 +195,7 @@
     @Captor private ArgumentCaptor<List<
             android.automotive.watchdog.internal.ResourceOveruseConfiguration>>
             mResourceOveruseConfigurationsCaptor;
+    @Captor private ArgumentCaptor<StatsPullAtomCallback> mStatsPullAtomCallbackCaptor;
     @Captor private ArgumentCaptor<int[]> mIntArrayCaptor;
     @Captor private ArgumentCaptor<byte[]> mOveruseStatsCaptor;
     @Captor private ArgumentCaptor<byte[]> mKilledStatsCaptor;
@@ -183,7 +205,6 @@
     @Captor private ArgumentCaptor<Integer> mSystemStateCaptor;
     @Captor private ArgumentCaptor<Integer> mKillReasonCaptor;
 
-
     private CarWatchdogService mCarWatchdogService;
     private ICarWatchdogServiceForSystem mWatchdogServiceForSystemImpl;
     private IBinder.DeathRecipient mCarWatchdogDaemonBinderDeathRecipient;
@@ -192,8 +213,10 @@
     private ICarPowerStateListener mCarPowerStateListener;
     private ICarPowerPolicyListener mCarPowerPolicyListener;
     private ICarUxRestrictionsChangeListener mCarUxRestrictionsChangeListener;
-    private TimeSourceInterface mTimeSource;
+    private StatsPullAtomCallback mStatsPullAtomCallback;
+    private File mTempSystemCarDir;
 
+    private final TestTimeSource mTimeSource = new TestTimeSource();
     private final SparseArray<String> mGenericPackageNameByUid = new SparseArray<>();
     private final SparseArray<List<String>> mPackagesBySharedUid = new SparseArray<>();
     private final ArrayMap<String, android.content.pm.PackageInfo> mPmPackageInfoByUserPackage =
@@ -202,6 +225,10 @@
     private final List<WatchdogStorage.UserPackageSettingsEntry> mUserPackageSettingsEntries =
             new ArrayList<>();
     private final List<WatchdogStorage.IoUsageStatsEntry> mIoUsageStatsEntries = new ArrayList<>();
+    private final List<AtomsProto.CarWatchdogSystemIoUsageSummary> mPulledSystemIoUsageSummaries =
+            new ArrayList<>();
+    private final List<AtomsProto.CarWatchdogUidIoUsageSummary> mPulledUidIoUsageSummaries =
+            new ArrayList<>();
     private final IPackageManager mSpiedPackageManager = spy(ActivityThread.getPackageManager());
 
     @Override
@@ -220,28 +247,33 @@
     @Before
     public void setUp() throws Exception {
         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockContext.getSystemService(StatsManager.class)).thenReturn(mMockStatsManager);
         when(mMockContext.getPackageName()).thenReturn(
                 CarWatchdogServiceUnitTest.class.getCanonicalName());
+        doReturn(mMockSystemInterface)
+                .when(() -> CarLocalServices.getService(SystemInterface.class));
         doReturn(mMockCarPowerManagementService)
                 .when(() -> CarLocalServices.getService(CarPowerManagementService.class));
         doReturn(mMockCarUxRestrictionsManagerService)
                 .when(() -> CarLocalServices.getService(CarUxRestrictionsManagerService.class));
         doReturn(mSpiedPackageManager).when(() -> ActivityThread.getPackageManager());
 
-        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage);
-        mCarWatchdogService.setOveruseHandlingDelay(OVERUSE_HANDLING_DELAY_MILLS);
-        mCarWatchdogService.setRecurringOveruseThreshold(RECURRING_OVERUSE_THRESHOLD);
-        setDate(/* numDaysAgo= */ 0);
+        when(mMockCarUxRestrictionsManagerService.getCurrentUxRestrictions())
+                .thenReturn(new CarUxRestrictions.Builder(/* reqOpt= */ false,
+                        UX_RESTRICTIONS_BASELINE, /* time= */ 0).build());
+
+        mTempSystemCarDir = Files.createTempDirectory("watchdog_test").toFile();
+        when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempSystemCarDir);
+
+        setupUsers();
         mockWatchdogDaemon();
         mockWatchdogStorage();
-        setupUsers();
-        mCarWatchdogService.init();
-        captureCarPowerListeners();
-        captureBroadcastReceiver();
-        captureCarUxRestrictionsChangeListener();
-        captureAndVerifyRegistrationWithDaemon(/* waitOnMain= */ true);
-        verifyDatabaseInit(/* wantedInvocations= */ 1);
         mockPackageManager();
+        mockBuildStatsEventCalls();
+
+        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage,
+                mTimeSource);
+        initService(/* wantedInvocations= */ 1);
     }
 
     /**
@@ -263,6 +295,11 @@
         mPackagesBySharedUid.clear();
         mPmPackageInfoByUserPackage.clear();
         mDisabledUserPackages.clear();
+        mPulledSystemIoUsageSummaries.clear();
+        mPulledUidIoUsageSummaries.clear();
+        if (mTempSystemCarDir != null) {
+            FileUtils.deleteContentsAndDir(mTempSystemCarDir);
+        }
     }
 
     @Test
@@ -396,7 +433,7 @@
         injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
                 packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
 
-        long startTime = mTimeSource.now().atZone(ZONE_OFFSET).minusDays(4).toEpochSecond();
+        long startTime = mTimeSource.getCurrentDateTime().minusDays(4).toEpochSecond();
         long duration = mTimeSource.now().getEpochSecond() - startTime;
         when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
                 UserHandle.getUserId(uid), packageName, 6))
@@ -461,7 +498,7 @@
         injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
                 packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
 
-        long startTime = mTimeSource.now().atZone(ZONE_OFFSET).minusDays(4).toEpochSecond();
+        long startTime = mTimeSource.getCurrentDateTime().minusDays(4).toEpochSecond();
         long duration = mTimeSource.now().getEpochSecond() - startTime;
         when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
                 UserHandle.getUserId(uid), packageName, 6))
@@ -576,7 +613,7 @@
                                 /* totalOveruses= */ 0)));
         pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
-        ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+        ZonedDateTime now = mTimeSource.getCurrentDateTime();
         long startTime = now.minusDays(4).toEpochSecond();
         IoOveruseStats thirdPartyPkgOldStats = new IoOveruseStats.Builder(
                 startTime, now.toEpochSecond() - startTime).setTotalOveruses(5)
@@ -761,7 +798,7 @@
                                 /* totalOveruses= */ 0)));
         pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
-        ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+        ZonedDateTime now = mTimeSource.getCurrentDateTime();
         long startTime = now.minusDays(4).toEpochSecond();
         IoOveruseStats thirdPartyPkgOldStats = new IoOveruseStats.Builder(
                 startTime, now.toEpochSecond() - startTime).setTotalOveruses(5)
@@ -855,7 +892,7 @@
                                 /* totalOveruses= */ 3)));
         pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
-        ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+        ZonedDateTime now = mTimeSource.getCurrentDateTime();
         long startTime = now.minusDays(4).toEpochSecond();
         IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
                 startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
@@ -2030,14 +2067,7 @@
         mCarWatchdogService.setKillablePackageAsUser(
                 "third_party_package.A", new UserHandle(12), /* isKillable= */ false);
 
-        setCarPowerState(CarPowerStateListener.SHUTDOWN_ENTER);
-        verify(mMockWatchdogStorage).saveIoUsageStats(any());
-        verify(mMockWatchdogStorage).saveUserPackageSettings(any());
-        mCarWatchdogService.release();
-        verify(mMockWatchdogStorage).release();
-        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage);
-        mCarWatchdogService.init();
-        verifyDatabaseInit(/* wantedInvocations= */ 2);
+        restartService(/* totalRestarts= */ 1);
 
         List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
@@ -2078,7 +2108,7 @@
                 constructPackageManagerPackageInfo("third_party_package", 1001100, null)));
 
         setDisplayStateEnabled(false);
-        setDate(1);
+        mTimeSource.updateNow(/* numDaysAgo= */ 1);
         List<PackageIoOveruseStats> prevDayStats = Arrays.asList(
                 constructPackageIoOveruseStats(1011200, /* shouldNotify= */ false,
                         /* forgivenWriteBytes= */ constructPerStateBytes(600, 700, 800),
@@ -2105,7 +2135,7 @@
                                 /* totalTimesKilled= */ 0)));
 
         setDisplayStateEnabled(true);
-        setDate(0);
+        mTimeSource.updateNow(/* numDaysAgo= */ 0);
         List<PackageIoOveruseStats> currentDayStats = Arrays.asList(
                 constructPackageIoOveruseStats(1011200, /* shouldNotify= */ false,
                         /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
@@ -2337,7 +2367,7 @@
     @Test
     public void testDisableRecurrentlyOverusingAppWhenDisplayDisabledAfterDateChange()
             throws Exception {
-        setDate(1);
+        mTimeSource.updateNow(/* numDaysAgo= */ 1);
         setUpSampleUserAndPackages();
         setRequiresDistractionOptimization(true);
         setDisplayStateEnabled(true);
@@ -2349,7 +2379,7 @@
 
         assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
 
-        setDate(0);
+        mTimeSource.updateNow(/* numDaysAgo= */ 0);
 
         pushLatestIoOveruseStatsAndWait(new ArrayList<>());
 
@@ -2606,6 +2636,190 @@
     }
 
     @Test
+    public void testPullSystemIoUsageSummaryAtomsWithRestart() throws Exception {
+        List<StatsEvent> events = new ArrayList<>();
+        assertWithMessage("Stats pull atom callback status")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        List<AtomsProto.CarWatchdogSystemIoUsageSummary> expectedSummaries =
+                verifyAndGetSystemIoUsageSummaries(
+                        mTimeSource.getCurrentDate().minus(RETENTION_PERIOD));
+
+        assertWithMessage("First pulled system I/O usage summary atoms")
+                .that(mPulledSystemIoUsageSummaries).containsExactlyElementsIn(expectedSummaries);
+        mPulledSystemIoUsageSummaries.clear();
+
+        restartService(/* totalRestarts= */ 1);
+
+        assertWithMessage("Status of stats pull atom callback after restart")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        assertWithMessage("Pulled system I/O usage summary atoms after restart")
+                .that(mPulledSystemIoUsageSummaries).isEmpty();
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
+    }
+
+    @Test
+    public void testPullSystemIoUsageSummaryAtomsWithDateChange() throws Exception {
+        mTimeSource.updateNow(/* numDaysAgo= */ 7);
+
+        List<StatsEvent> events = new ArrayList<>();
+        assertWithMessage("Stats pull atom callback status")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        List<AtomsProto.CarWatchdogSystemIoUsageSummary> expectedSummaries =
+                verifyAndGetSystemIoUsageSummaries(
+                        mTimeSource.getCurrentDate().minus(RETENTION_PERIOD));
+
+        assertWithMessage("First pulled system I/O usage summary atoms")
+                .that(mPulledSystemIoUsageSummaries).containsExactlyElementsIn(expectedSummaries);
+        mPulledSystemIoUsageSummaries.clear();
+
+        mTimeSource.updateNow(/* numDaysAgo= */ 6);
+
+        assertWithMessage("Status of stats pull atom callback within the same week")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        assertWithMessage("Pulled system I/O usage summary atoms within the same week")
+                .that(mPulledSystemIoUsageSummaries).isEmpty();
+
+        mTimeSource.updateNow(/* numDaysAgo= */ 0);
+
+        assertWithMessage("Status of stats pull atom callback after a week")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        expectedSummaries = verifyAndGetSystemIoUsageSummaries(
+                mTimeSource.getCurrentDate().minus(1, ChronoUnit.WEEKS));
+
+        assertWithMessage("Pulled system I/O usage summary atoms after a week")
+                .that(mPulledSystemIoUsageSummaries).containsExactlyElementsIn(expectedSummaries);
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
+    }
+
+    @Test
+    public void testPullUidIoUsageSummaryAtomsForTopUids() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("system_package.critical.A", 10000345, null),
+                constructPackageManagerPackageInfo("third_party_package.B", 10004675, null),
+                constructPackageManagerPackageInfo("system_package.critical.B", 10010001, null),
+                constructPackageManagerPackageInfo("vendor_package.non_critical", 10110004, null),
+                constructPackageManagerPackageInfo("third_party_package.A", 10110005,
+                        "third_party_shared_package")));
+
+        List<StatsEvent> events = new ArrayList<>();
+        assertWithMessage("Stats pull atom callback status")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        List<AtomsProto.CarWatchdogUidIoUsageSummary> expectedSummaries =
+                verifyAndGetUidIoUsageSummaries(
+                        mTimeSource.getCurrentDate().minus(RETENTION_PERIOD),
+                        /* expectUids= */ Arrays.asList(10010001, 10110004, 10110005));
+
+        assertWithMessage(String.format("Pulled uid I/O usage summary atoms for top %d UIDs",
+                UID_IO_USAGE_SUMMARY_TOP_COUNT)).that(mPulledUidIoUsageSummaries)
+                .containsExactlyElementsIn(expectedSummaries);
+    }
+
+    @Test
+    public void testPullUidIoUsageSummaryAtomsWithRestart() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("system_package.critical", 10010001, null),
+                constructPackageManagerPackageInfo("vendor_package.non_critical", 10110004, null),
+                constructPackageManagerPackageInfo("third_party_package.A", 10110005,
+                        "third_party_shared_package")));
+
+        List<StatsEvent> events = new ArrayList<>();
+        assertWithMessage("Stats pull atom callback status")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        List<AtomsProto.CarWatchdogUidIoUsageSummary> expectedSummaries =
+                verifyAndGetUidIoUsageSummaries(
+                        mTimeSource.getCurrentDate().minus(RETENTION_PERIOD),
+                        /* expectUids= */ Arrays.asList(10010001, 10110004, 10110005));
+
+        assertWithMessage("First pulled uid I/O usage summary atoms")
+                .that(mPulledUidIoUsageSummaries).containsExactlyElementsIn(expectedSummaries);
+        mPulledUidIoUsageSummaries.clear();
+
+        restartService(/* totalRestarts= */ 1);
+
+        assertWithMessage("Status of stats pull atom callback after restart")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        assertWithMessage("Pulled uid I/O usage summary atoms after restart")
+                .that(mPulledUidIoUsageSummaries).isEmpty();
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
+    }
+
+    @Test
+    public void testPullUidIoUsageSummaryAtomsWithDateChange() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("system_package.critical", 10010001, null),
+                constructPackageManagerPackageInfo("vendor_package.non_critical", 10110004, null),
+                constructPackageManagerPackageInfo("third_party_package.A", 10110005,
+                        "third_party_shared_package")));
+
+        mTimeSource.updateNow(/* numDaysAgo= */ 7);
+
+        List<StatsEvent> events = new ArrayList<>();
+        assertWithMessage("Stats pull atom callback status")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        List<AtomsProto.CarWatchdogUidIoUsageSummary> expectedSummaries =
+                verifyAndGetUidIoUsageSummaries(
+                        mTimeSource.getCurrentDate().minus(RETENTION_PERIOD),
+                        /* expectUids= */ Arrays.asList(10010001, 10110004, 10110005));
+
+        assertWithMessage("First pulled uid I/O usage summary atoms")
+                .that(mPulledUidIoUsageSummaries).containsExactlyElementsIn(expectedSummaries);
+        mPulledUidIoUsageSummaries.clear();
+
+        mTimeSource.updateNow(/* numDaysAgo= */ 6);
+
+        assertWithMessage("Status of stats pull atom callback within the same week")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        assertWithMessage("Pulled uid I/O usage summary atoms within the same week")
+                .that(mPulledUidIoUsageSummaries).isEmpty();
+
+        mTimeSource.updateNow(/* numDaysAgo= */ 0);
+
+        assertWithMessage("Status of stats pull atom callback after a week")
+                .that(mStatsPullAtomCallback.onPullAtom(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY,
+                        events)).isEqualTo(PULL_SUCCESS);
+
+        expectedSummaries = verifyAndGetUidIoUsageSummaries(
+                mTimeSource.getCurrentDate().minus(1, ChronoUnit.WEEKS),
+                /* expectUids= */ Arrays.asList(10010001, 10110004, 10110005));
+
+        assertWithMessage("Pulled uid I/O usage summary atoms after a week")
+                .that(mPulledUidIoUsageSummaries).containsExactlyElementsIn(expectedSummaries);
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
+    }
+
+    @Test
+    public void testPullInvalidAtoms() throws Exception {
+        List<StatsEvent> actualEvents = new ArrayList<>();
+        assertWithMessage("Stats pull atom callback status").that(mStatsPullAtomCallback.onPullAtom(
+                0, actualEvents)).isEqualTo(PULL_SKIP);
+        assertWithMessage("Pulled stats events").that(actualEvents).isEmpty();
+    }
+
+    @Test
     public void testGetPackageInfosForUids() throws Exception {
         injectPackageInfos(Arrays.asList(
                 constructPackageManagerPackageInfo(
@@ -2939,6 +3153,24 @@
         });
         when(mMockWatchdogStorage.getUserPackageSettings()).thenReturn(mUserPackageSettingsEntries);
         when(mMockWatchdogStorage.getTodayIoUsageStats()).thenReturn(mIoUsageStatsEntries);
+        when(mMockWatchdogStorage.getDailySystemIoUsageSummaries(anyLong(), anyLong())).thenAnswer(
+                args -> sampleDailyIoUsageSummariesForAWeek(args.getArgument(0),
+                        SYSTEM_DAILY_IO_USAGE_SUMMARY_MULTIPLIER));
+        when(mMockWatchdogStorage.getTopUsersDailyIoUsageSummaries(
+                anyInt(), anyLong(), anyLong(), anyLong())).thenAnswer(args -> {
+                    ArrayList<WatchdogStorage.UserPackageDailySummaries> summaries =
+                            new ArrayList<>();
+                    for (int i = 0; i < mGenericPackageNameByUid.size(); ++i) {
+                        int uid = mGenericPackageNameByUid.keyAt(i);
+                        summaries.add(new WatchdogStorage.UserPackageDailySummaries(
+                                UserHandle.getUserId(uid), mGenericPackageNameByUid.valueAt(i),
+                                sampleDailyIoUsageSummariesForAWeek(args.getArgument(2),
+                                        /* sysOrUidMultiplier= */ uid)));
+                    }
+                    summaries.sort(Comparator.comparingLong(WatchdogStorage
+                            .UserPackageDailySummaries::getTotalWrittenBytes).reversed());
+                    return summaries;
+                });
     }
 
     private void setupUsers() {
@@ -2946,29 +3178,59 @@
         mockUmGetAllUsers(mMockUserManager, new UserInfo[0]);
     }
 
-    private void captureCarPowerListeners() {
-        verify(mMockCarPowerManagementService).registerListener(
+    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();
+        captureCarPowerListeners(wantedInvocations);
+        captureBroadcastReceiver(wantedInvocations);
+        captureCarUxRestrictionsChangeListener(wantedInvocations);
+        captureAndVerifyRegistrationWithDaemon(/* waitOnMain= */ true);
+        verifyDatabaseInit(wantedInvocations);
+        captureStatsPullAtomCallback(wantedInvocations);
+    }
+
+    private void restartService(int totalRestarts) throws Exception {
+        setCarPowerState(CarPowerStateListener.SHUTDOWN_PREPARE);
+        setCarPowerState(CarPowerStateListener.SHUTDOWN_ENTER);
+        mCarWatchdogService.release();
+        verify(mMockWatchdogStorage, times(totalRestarts)).saveIoUsageStats(any());
+        verify(mMockWatchdogStorage, times(totalRestarts)).saveUserPackageSettings(any());
+        verify(mMockWatchdogStorage, times(totalRestarts)).release();
+        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage,
+                mTimeSource);
+        initService(/* wantedInvocations= */ totalRestarts + 1);
+    }
+
+    private void captureCarPowerListeners(int wantedInvocations) {
+        verify(mMockCarPowerManagementService, times(wantedInvocations)).registerListener(
                 mICarPowerStateListenerCaptor.capture());
         mCarPowerStateListener = mICarPowerStateListenerCaptor.getValue();
         assertWithMessage("Car power state listener").that(mCarPowerStateListener).isNotNull();
 
-        verify(mMockCarPowerManagementService).addPowerPolicyListener(
+        verify(mMockCarPowerManagementService, times(wantedInvocations)).addPowerPolicyListener(
                 any(), mICarPowerPolicyListenerCaptor.capture());
         mCarPowerPolicyListener = mICarPowerPolicyListenerCaptor.getValue();
         assertWithMessage("Car power policy listener").that(mCarPowerPolicyListener).isNotNull();
     }
 
-    private void captureBroadcastReceiver() {
-        verify(mMockContext)
+    private void captureBroadcastReceiver(int wantedInvocations) {
+        verify(mMockContext, times(wantedInvocations))
                 .registerReceiverForAllUsers(mBroadcastReceiverCaptor.capture(), any(), any(),
                         any());
         mBroadcastReceiver = mBroadcastReceiverCaptor.getValue();
         assertWithMessage("Broadcast receiver").that(mBroadcastReceiver).isNotNull();
     }
 
-    private void captureCarUxRestrictionsChangeListener() {
-        verify(mMockCarUxRestrictionsManagerService).registerUxRestrictionsChangeListener(
-                mICarUxRestrictionsChangeListener.capture(), eq(Display.DEFAULT_DISPLAY));
+    private void captureCarUxRestrictionsChangeListener(int wantedInvocations) {
+        verify(mMockCarUxRestrictionsManagerService, times(wantedInvocations))
+                .getCurrentUxRestrictions();
+        verify(mMockCarUxRestrictionsManagerService, times(wantedInvocations))
+                .registerUxRestrictionsChangeListener(mICarUxRestrictionsChangeListener.capture(),
+                        eq(Display.DEFAULT_DISPLAY));
         mCarUxRestrictionsChangeListener = mICarUxRestrictionsChangeListener.getValue();
         assertWithMessage("UX restrictions change listener").that(mCarUxRestrictionsChangeListener)
                 .isNotNull();
@@ -3016,6 +3278,44 @@
         verify(mMockWatchdogStorage, times(wantedInvocations)).getTodayIoUsageStats();
     }
 
+    private void captureStatsPullAtomCallback(int wantedInvocations) {
+        verify(mMockStatsManager, times(wantedInvocations)).setPullAtomCallback(
+                eq(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY), any(PullAtomMetadata.class),
+                any(Executor.class), mStatsPullAtomCallbackCaptor.capture());
+        verify(mMockStatsManager, times(wantedInvocations)).setPullAtomCallback(
+                eq(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY), any(PullAtomMetadata.class),
+                any(Executor.class), mStatsPullAtomCallbackCaptor.capture());
+
+        // The same callback is set in the above calls, so fetch the latest captured callback.
+        mStatsPullAtomCallback = mStatsPullAtomCallbackCaptor.getValue();
+        assertWithMessage("Stats pull atom callback").that(mStatsPullAtomCallback).isNotNull();
+    }
+
+    private void mockBuildStatsEventCalls() {
+        when(CarStatsLog.buildStatsEvent(eq(CAR_WATCHDOG_SYSTEM_IO_USAGE_SUMMARY),
+                any(byte[].class))).thenAnswer(args -> {
+                    mPulledSystemIoUsageSummaries.add(AtomsProto.CarWatchdogSystemIoUsageSummary
+                            .newBuilder()
+                            .setIoUsageSummary(AtomsProto.CarWatchdogIoUsageSummary.parseFrom(
+                                    (byte[]) args.getArgument(1)))
+                            .build());
+                    // Returned event is not used in tests, so return an empty event.
+                    return StatsEvent.newBuilder().build();
+                });
+
+        when(CarStatsLog.buildStatsEvent(eq(CAR_WATCHDOG_UID_IO_USAGE_SUMMARY), anyInt(),
+                any(byte[].class))).thenAnswer(args -> {
+                    mPulledUidIoUsageSummaries.add(AtomsProto.CarWatchdogUidIoUsageSummary
+                            .newBuilder()
+                            .setUid(args.getArgument(1))
+                            .setIoUsageSummary(AtomsProto.CarWatchdogIoUsageSummary.parseFrom(
+                                    (byte[]) args.getArgument(2)))
+                            .build());
+                    // Returned event is not used in tests, so return an empty event.
+                    return StatsEvent.newBuilder().build();
+                });
+    }
+
     private void mockPackageManager() throws Exception {
         when(mMockPackageManager.getNamesForUids(any())).thenAnswer(args -> {
             int[] uids = args.getArgument(0);
@@ -3042,10 +3342,9 @@
                     }
                     return packageInfo.applicationInfo;
                 });
-        when(mMockPackageManager.getPackageInfoAsUser(anyString(), anyInt(), anyInt())).thenAnswer(
-                args -> {
-                    int userId = args.getArgument(2);
-                    String userPackageId = userId + ":" + args.getArgument(0);
+        when(mMockPackageManager.getPackageInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenAnswer(args -> {
+                    String userPackageId = args.getArgument(2) + ":" + args.getArgument(0);
                     android.content.pm.PackageInfo packageInfo =
                             mPmPackageInfoByUserPackage.get(userPackageId);
                     if (packageInfo == null) {
@@ -3054,8 +3353,8 @@
                     }
                     return packageInfo;
                 });
-        when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt())).thenAnswer(
-                args -> {
+        when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt()))
+                .thenAnswer(args -> {
                     int userId = args.getArgument(1);
                     List<android.content.pm.PackageInfo> packageInfos = new ArrayList<>();
                     for (android.content.pm.PackageInfo packageInfo :
@@ -3066,6 +3365,18 @@
                     }
                     return packageInfos;
                 });
+        when(mMockPackageManager.getPackageUidAsUser(anyString(), anyInt()))
+                .thenAnswer(args -> {
+                    String userPackageId = args.getArgument(1) + ":" + args.getArgument(0);
+                    android.content.pm.PackageInfo packageInfo =
+                            mPmPackageInfoByUserPackage.get(userPackageId);
+                    if (packageInfo == null) {
+                        throw new PackageManager.NameNotFoundException(
+                                "User package id '" + userPackageId + "' not found");
+                    }
+                    return packageInfo.applicationInfo.uid;
+                });
+
         doAnswer((args) -> {
             String value = args.getArgument(3) + ":" + args.getArgument(0);
             mDisabledUserPackages.add(value);
@@ -3118,26 +3429,6 @@
         captureAndVerifyRegistrationWithDaemon(/* waitOnMain= */ false);
     }
 
-    private void setDate(int numDaysAgo) {
-        TimeSourceInterface timeSource = new TimeSourceInterface() {
-            @Override
-            public Instant now() {
-                /* Return the same time, so the tests are deterministic. */
-                return mNow;
-            }
-
-            @Override
-            public String toString() {
-                return "Mocked date to " + now();
-            }
-
-            private final Instant mNow = Instant.now().minus(numDaysAgo, ChronoUnit.DAYS);
-        };
-        mCarWatchdogService.setTimeSource(timeSource);
-        mTimeSource = timeSource;
-    }
-
-
     private void testClientHealthCheck(TestClient client, int badClientCount) throws Exception {
         mCarWatchdogService.registerClient(client, TIMEOUT_CRITICAL);
         mWatchdogServiceForSystemImpl.checkIfAlive(123456, TIMEOUT_CRITICAL);
@@ -3180,8 +3471,7 @@
         return packageIoOveruseStatsByUid;
     }
 
-    private void injectPackageInfos(
-            List<android.content.pm.PackageInfo> packageInfos) {
+    private void injectPackageInfos(List<android.content.pm.PackageInfo> packageInfos) {
         for (android.content.pm.PackageInfo packageInfo : packageInfos) {
             String genericPackageName = packageInfo.packageName;
             int uid = packageInfo.applicationInfo.uid;
@@ -3208,8 +3498,8 @@
         }
     }
 
-    private void pushLatestIoOveruseStatsAndWait(
-            List<PackageIoOveruseStats> packageIoOveruseStats) throws Exception {
+    private void pushLatestIoOveruseStatsAndWait(List<PackageIoOveruseStats> packageIoOveruseStats)
+            throws Exception {
         mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
 
         // Resource overuse handling is done on the main thread by posting a new message with
@@ -3661,9 +3951,7 @@
                     AtomsProto.CarWatchdogIoOveruseStats.parseFrom(allOveruseStatsValues.get(i))));
         }
         assertWithMessage("I/O overuse stats reported to statsd").that(actual)
-                .comparingElementsUsing(Correspondence.from(
-                        CarWatchdogServiceUnitTest::isCarWatchdogIoOveruseStatsReportedEquals,
-                        "is equal to")).containsExactlyElementsIn(expected);
+                .containsExactlyElementsIn(expected);
     }
 
     private void captureAndVerifyKillStatsReported(
@@ -3691,55 +3979,87 @@
                             allIoOveruseStatsValues.get(i))));
         }
         assertWithMessage("I/O overuse kill stats reported to statsd").that(actual)
-                .comparingElementsUsing(Correspondence.from(
-                        CarWatchdogServiceUnitTest::isCarWatchdogKillStatsReported, "is equal to"))
                 .containsExactlyElementsIn(expected);
     }
 
-    public static boolean isCarWatchdogIoOveruseStatsReportedEquals(
-            AtomsProto.CarWatchdogIoOveruseStatsReported actual,
-            AtomsProto.CarWatchdogIoOveruseStatsReported expected) {
-        return actual.getUid() == expected.getUid()
-                && isCarWatchdogIoOveruseStatsEquals(
-                actual.getIoOveruseStats(), expected.getIoOveruseStats());
-    }
-
-    public static boolean isCarWatchdogKillStatsReported(
-            AtomsProto.CarWatchdogKillStatsReported actual,
-            AtomsProto.CarWatchdogKillStatsReported expected) {
-        return actual.getUid() == expected.getUid()
-                && actual.getUidState() == expected.getUidState()
-                && actual.getSystemState() == expected.getSystemState()
-                && actual.getKillReason() == expected.getKillReason()
-                // Process stats is null on I/O overuse kill stats, so equality checking on the
-                // objects is sufficient.
-                && actual.getProcessStats().equals(expected.getProcessStats())
-                && isCarWatchdogIoOveruseStatsEquals(
-                actual.getIoOveruseStats(), expected.getIoOveruseStats());
-    }
-
-    private static boolean isCarWatchdogIoOveruseStatsEquals(
-            AtomsProto.CarWatchdogIoOveruseStats actual,
-            AtomsProto.CarWatchdogIoOveruseStats expected) {
-        if (actual == null || expected == null) {
-            return (actual == null) && (expected == null);
+    private List<AtomsProto.CarWatchdogSystemIoUsageSummary> verifyAndGetSystemIoUsageSummaries(
+            ZonedDateTime beginReportDate) {
+        ZonedDateTime beginWeekStartDate = beginReportDate.with(ChronoField.DAY_OF_WEEK, 1);
+        ZonedDateTime endWeekStartDate = mTimeSource.getCurrentDate()
+                .with(ChronoField.DAY_OF_WEEK, 1);
+        List<AtomsProto.CarWatchdogSystemIoUsageSummary> expectedSummaries = new ArrayList<>();
+        while (!beginWeekStartDate.equals(endWeekStartDate)) {
+            long startEpochSecond = beginWeekStartDate.toEpochSecond();
+            verify(mMockWatchdogStorage).getDailySystemIoUsageSummaries(startEpochSecond,
+                    beginWeekStartDate.plusWeeks(1).toEpochSecond());
+            expectedSummaries.add(AtomsProto.CarWatchdogSystemIoUsageSummary.newBuilder()
+                    .setIoUsageSummary(constructCarWatchdogIoUsageSummary(startEpochSecond,
+                            sampleDailyIoUsageSummariesForAWeek(startEpochSecond,
+                                    SYSTEM_DAILY_IO_USAGE_SUMMARY_MULTIPLIER)))
+                    .build());
+            beginWeekStartDate = beginWeekStartDate.plusWeeks(1);
         }
-        return actual.getPeriod() == expected.getPeriod()
-                && isCarWatchdogPerStateBytesEquals(actual.getThreshold(), expected.getThreshold())
-                && isCarWatchdogPerStateBytesEquals(
-                actual.getWrittenBytes(), expected.getWrittenBytes())
-                && actual.getUptimeMillis() == expected.getUptimeMillis();
+        return expectedSummaries;
     }
 
-    private static boolean isCarWatchdogPerStateBytesEquals(
-            AtomsProto.CarWatchdogPerStateBytes actual,
-            AtomsProto.CarWatchdogPerStateBytes expected) {
-        if (actual == null || expected == null) {
-            return (actual == null) && (expected == null);
+    private List<AtomsProto.CarWatchdogUidIoUsageSummary> verifyAndGetUidIoUsageSummaries(
+            ZonedDateTime beginReportDate, List<Integer> expectUids) {
+        ZonedDateTime beginWeekStartDate = beginReportDate.with(ChronoField.DAY_OF_WEEK, 1);
+        ZonedDateTime endWeekStartDate = mTimeSource.getCurrentDate()
+                .with(ChronoField.DAY_OF_WEEK, 1);
+        List<AtomsProto.CarWatchdogUidIoUsageSummary> expectedSummaries = new ArrayList<>();
+        while (!beginWeekStartDate.equals(endWeekStartDate)) {
+            long startEpochSecond = beginWeekStartDate.toEpochSecond();
+            verify(mMockWatchdogStorage).getTopUsersDailyIoUsageSummaries(
+                    UID_IO_USAGE_SUMMARY_TOP_COUNT * 2,
+                    UID_IO_USAGE_SUMMARY_MIN_WEEKLY_WRITTEN_BYTES, startEpochSecond,
+                    beginWeekStartDate.plusWeeks(1).toEpochSecond());
+            for (Integer uid : expectUids) {
+                expectedSummaries.add(AtomsProto.CarWatchdogUidIoUsageSummary.newBuilder()
+                        .setUid(uid)
+                        .setIoUsageSummary(constructCarWatchdogIoUsageSummary(startEpochSecond,
+                                sampleDailyIoUsageSummariesForAWeek(startEpochSecond,
+                                        uid)))
+                        .build());
+            }
+            beginWeekStartDate = beginWeekStartDate.plusWeeks(1);
         }
-        return actual.getForegroundBytes() == expected.getForegroundBytes()
-                && actual.getBackgroundBytes() == expected.getBackgroundBytes()
-                && actual.getGarageModeBytes() == expected.getGarageModeBytes();
+        return expectedSummaries;
+    }
+
+    private static AtomsProto.CarWatchdogIoUsageSummary constructCarWatchdogIoUsageSummary(
+            long startTimeMillis, List<AtomsProto.CarWatchdogDailyIoUsageSummary> dailySummaries) {
+        return AtomsProto.CarWatchdogIoUsageSummary.newBuilder()
+                .setEventTimePeriod(AtomsProto.CarWatchdogEventTimePeriod.newBuilder()
+                        .setPeriod(AtomsProto.CarWatchdogEventTimePeriod.Period.WEEKLY)
+                        .setStartTimeMillis(startTimeMillis).build())
+                .addAllDailyIoUsageSummary(dailySummaries)
+                .build();
+    }
+
+    private List<AtomsProto.CarWatchdogDailyIoUsageSummary> sampleDailyIoUsageSummariesForAWeek(
+            long startEpochSeconds, long sysOrUidMultiplier) {
+        List<AtomsProto.CarWatchdogDailyIoUsageSummary> summaries = new ArrayList<>();
+        long weekMultiplier = ChronoUnit.WEEKS.between(
+                ZonedDateTime.ofInstant(Instant.ofEpochSecond(startEpochSeconds), ZONE_OFFSET),
+                mTimeSource.getCurrentDate());
+        for (int i = 1; i < 8; ++i) {
+            summaries.add(constructCarWatchdogDailyIoUsageSummary(
+                    /* fgWrBytes= */ 100 * i * weekMultiplier * sysOrUidMultiplier,
+                    /* bgWrBytes= */ 200 * i * weekMultiplier * sysOrUidMultiplier,
+                    /* gmWrBytes= */ 300 * i * weekMultiplier * sysOrUidMultiplier,
+                    /* overuseCount= */ 2 * i));
+        }
+        return summaries;
+    }
+
+    static AtomsProto.CarWatchdogDailyIoUsageSummary constructCarWatchdogDailyIoUsageSummary(
+            long fgWrBytes, long bgWrBytes, long gmWrBytes, int overuseCount) {
+        return AtomsProto.CarWatchdogDailyIoUsageSummary.newBuilder()
+                .setWrittenBytes(WatchdogPerfHandler
+                        .constructCarWatchdogPerStateBytes(fgWrBytes, bgWrBytes, gmWrBytes))
+                .setOveruseCount(overuseCount)
+                .build();
     }
 
     private class TestClient extends ICarWatchdogServiceCallback.Stub {
@@ -3788,13 +4108,6 @@
         return packageInfo;
     }
 
-    private static ApplicationInfo constructApplicationInfo(int flags, int privateFlags) {
-        ApplicationInfo applicationInfo = new ApplicationInfo();
-        applicationInfo.flags = flags;
-        applicationInfo.privateFlags = privateFlags;
-        return applicationInfo;
-    }
-
     private static String toPackageInfosString(List<PackageInfo> packageInfos) {
         StringBuilder builder = new StringBuilder();
         for (PackageInfo packageInfo : packageInfos) {
@@ -3877,4 +4190,27 @@
         packageInfo.applicationInfo.privateFlags = privateFlags;
         return packageInfo;
     }
+
+    private static final class TestTimeSource extends TimeSource {
+        private static final Instant TEST_DATE_TIME = Instant.parse("2021-11-12T13:14:15.16Z");
+        private Instant mNow;
+        TestTimeSource() {
+            mNow = TEST_DATE_TIME;
+        }
+
+        @Override
+        public Instant now() {
+            /* Return the same time, so the tests are deterministic. */
+            return mNow;
+        }
+
+        @Override
+        public String toString() {
+            return "Mocked date to " + now();
+        }
+
+        void updateNow(int numDaysAgo) {
+            mNow = TEST_DATE_TIME.minus(numDaysAgo, ChronoUnit.DAYS);
+        }
+    }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/UserPackageSettingsEntrySubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/UserPackageSettingsEntrySubject.java
deleted file mode 100644
index aa1df9e..0000000
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/UserPackageSettingsEntrySubject.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2021 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.car.watchdog;
-
-import static com.google.common.truth.Truth.assertAbout;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.annotation.Nullable;
-
-import com.google.common.truth.Correspondence;
-import com.google.common.truth.FailureMetadata;
-import com.google.common.truth.Subject;
-
-import java.util.Arrays;
-
-public final class UserPackageSettingsEntrySubject extends Subject {
-    /* Boiler-plate Subject.Factory for UserPackageSettingsEntrySubject. */
-    private static final Subject.Factory<
-            com.android.car.watchdog.UserPackageSettingsEntrySubject,
-            Iterable<WatchdogStorage.UserPackageSettingsEntry>>
-            USER_PACKAGE_SETTINGS_ENTRY_SUBJECT_FACTORY =
-            com.android.car.watchdog.UserPackageSettingsEntrySubject::new;
-
-    private final Iterable<WatchdogStorage.UserPackageSettingsEntry> mActual;
-
-    /* User-defined entry point. */
-    public static UserPackageSettingsEntrySubject assertThat(
-            @Nullable Iterable<WatchdogStorage.UserPackageSettingsEntry> stats) {
-        return assertAbout(USER_PACKAGE_SETTINGS_ENTRY_SUBJECT_FACTORY).that(stats);
-    }
-
-    public static Subject.Factory<UserPackageSettingsEntrySubject,
-            Iterable<WatchdogStorage.UserPackageSettingsEntry>> userPackageSettingsEntries() {
-        return USER_PACKAGE_SETTINGS_ENTRY_SUBJECT_FACTORY;
-    }
-
-    public void containsExactly(WatchdogStorage.UserPackageSettingsEntry... stats) {
-        containsExactlyElementsIn(Arrays.asList(stats));
-    }
-
-    public void containsExactlyElementsIn(
-            Iterable<WatchdogStorage.UserPackageSettingsEntry> expected) {
-        assertWithMessage("Expected entries (%s) equals to actual entries (%s)",
-                toString(expected), toString(mActual)).that(mActual)
-                .comparingElementsUsing(Correspondence.from(
-                        UserPackageSettingsEntrySubject::isEquals, "is equal to"))
-                .containsExactlyElementsIn(expected);
-    }
-
-    public static boolean isEquals(WatchdogStorage.UserPackageSettingsEntry actual,
-            WatchdogStorage.UserPackageSettingsEntry expected) {
-        if (actual == null || expected == null) {
-            return (actual == null) && (expected == null);
-        }
-        return actual.userId == expected.userId && actual.packageName.equals(expected.packageName)
-                && actual.killableState == expected.killableState;
-    }
-
-    private static String toString(Iterable<WatchdogStorage.UserPackageSettingsEntry> entries) {
-        StringBuilder builder = new StringBuilder();
-        builder.append("[");
-        for (WatchdogStorage.UserPackageSettingsEntry entry : entries) {
-            toStringBuilder(builder, entry).append(", ");
-        }
-        if (builder.length() > 1) {
-            builder.delete(builder.length() - 2, builder.length());
-        }
-        builder.append("]");
-        return builder.toString();
-    }
-
-    private static StringBuilder toStringBuilder(StringBuilder builder,
-            WatchdogStorage.UserPackageSettingsEntry entry) {
-        return builder.append("{UserId: ").append(entry.userId)
-                .append(", Package name: ").append(entry.packageName)
-                .append(", Killable state: ").append(entry.killableState).append("}");
-    }
-
-    private UserPackageSettingsEntrySubject(FailureMetadata failureMetadata,
-            @Nullable Iterable<WatchdogStorage.UserPackageSettingsEntry> iterableSubject) {
-        super(failureMetadata, iterableSubject);
-        this.mActual = iterableSubject;
-    }
-}
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 c84225d..32d824c 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
@@ -23,7 +23,6 @@
 import static com.android.car.watchdog.WatchdogStorage.RETENTION_PERIOD;
 import static com.android.car.watchdog.WatchdogStorage.STATS_TEMPORAL_UNIT;
 import static com.android.car.watchdog.WatchdogStorage.WatchdogDbHelper.DATABASE_NAME;
-import static com.android.car.watchdog.WatchdogStorage.ZONE_OFFSET;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -62,15 +61,16 @@
     private Context mContext;
     private WatchdogStorage mService;
     private File mDatabaseFile;
-    private TimeSourceInterface mTimeSource;
+
+    private final TestTimeSource mTimeSource = new TestTimeSource();
 
     @Before
     public void setUp() throws Exception {
+        mTimeSource.updateNow(/* numDaysAgo= */ 0);
         mContext = InstrumentationRegistry.getTargetContext().createDeviceProtectedStorageContext();
         mDatabaseFile = mContext.createDeviceProtectedStorageContext()
                 .getDatabasePath(DATABASE_NAME);
-        mService = new WatchdogStorage(mContext, /* useDataSystemCarDir= */ false);
-        setDate(/* numDaysAgo= */ 0);
+        mService = new WatchdogStorage(mContext, /* useDataSystemCarDir= */ false, mTimeSource);
     }
 
     @After
@@ -87,7 +87,7 @@
 
         assertThat(mService.saveUserPackageSettings(expected)).isTrue();
 
-        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+        assertWithMessage("User package settings").that(mService.getUserPackageSettings())
                 .containsExactlyElementsIn(expected);
     }
 
@@ -109,23 +109,21 @@
 
         assertThat(mService.saveUserPackageSettings(expected)).isTrue();
 
-        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
-                .containsExactlyElementsIn(expected);
+        assertWithMessage("User package settings after overwrite")
+                .that(mService.getUserPackageSettings()).containsExactlyElementsIn(expected);
     }
 
     @Test
     public void testSaveAndGetIoOveruseStats() throws Exception {
         injectSampleUserPackageSettings();
         /* Start time aligned to the beginning of the day. */
-        long startTime = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT)
-                .toEpochSecond();
+        long startTime = mTimeSource.getCurrentDate().toEpochSecond();
 
         assertWithMessage("Saved I/O usage stats successfully")
                 .that(mService.saveIoUsageStats(sampleStatsForDate(startTime, /* duration= */ 60)))
                 .isTrue();
 
-        long expectedDuration =
-                mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - startTime;
+        long expectedDuration = mTimeSource.getCurrentDateTime().toEpochSecond() - startTime;
         List<WatchdogStorage.IoUsageStatsEntry> expected = sampleStatsForDate(
                 startTime, expectedDuration);
 
@@ -137,18 +135,16 @@
     public void testSaveAndGetIoOveruseStatsWithOffsettedStartTime() throws Exception {
         injectSampleUserPackageSettings();
         /* Start time in the middle of the day. */
-        long startTime = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT)
-                .plusHours(12).toEpochSecond();
+        long startTime = mTimeSource.getCurrentDate().plusHours(12).toEpochSecond();
         List<WatchdogStorage.IoUsageStatsEntry> entries = sampleStatsForDate(
                 startTime, /* duration= */ 60);
 
         assertWithMessage("Saved I/O usage stats successfully")
                 .that(mService.saveIoUsageStats(entries)).isTrue();
 
-        long expectedStartTime = mTimeSource.now().atZone(ZONE_OFFSET)
-                .truncatedTo(STATS_TEMPORAL_UNIT).toEpochSecond();
+        long expectedStartTime = mTimeSource.getCurrentDate().toEpochSecond();
         long expectedDuration =
-                mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - expectedStartTime;
+                mTimeSource.getCurrentDateTime().toEpochSecond() - expectedStartTime;
         List<WatchdogStorage.IoUsageStatsEntry> expected = sampleStatsForDate(
                 expectedStartTime, expectedDuration);
 
@@ -159,9 +155,8 @@
     @Test
     public void testOverwriteIoOveruseStats() throws Exception {
         injectSampleUserPackageSettings();
-        long startTime = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT)
-                .toEpochSecond();
-        long duration = mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - startTime;
+        long startTime = mTimeSource.getCurrentDate().toEpochSecond();
+        long duration = mTimeSource.getCurrentDateTime().toEpochSecond() - startTime;
 
         List<WatchdogStorage.IoUsageStatsEntry> statsBeforeOverwrite = Collections.singletonList(
                 constructIoUsageStatsEntry(
@@ -200,8 +195,8 @@
                 .containsExactlyElementsIn(statsBeforeOverwrite);
 
         mService.release();
-        mService = new WatchdogStorage(mContext, /* useDataSystemCarDir= */ false);
-        setDate(/* numDaysAgo= */ 0);
+        mTimeSource.updateNow(/* numDaysAgo= */ 0);
+        mService = new WatchdogStorage(mContext, /* useDataSystemCarDir= */ false, mTimeSource);
 
         assertWithMessage("User packages settings").that(mService.getUserPackageSettings())
                 .isNotEmpty();
@@ -243,12 +238,11 @@
          * Returned stats shouldn't include stats for the current date as WatchdogPerfHandler fills
          * the current day's stats.
          */
-        ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
-                .truncatedTo(STATS_TEMPORAL_UNIT);
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
         long startTime = currentDate.minus(4, STATS_TEMPORAL_UNIT).toEpochSecond();
         long duration = currentDate.toEpochSecond() - startTime;
         IoOveruseStats expected = new IoOveruseStats.Builder(startTime, duration)
-                .setTotalOveruses(8).setTotalTimesKilled(4).setTotalBytesWritten(24_000).build();
+                .setTotalOveruses(8).setTotalTimesKilled(4).setTotalBytesWritten(25_200).build();
 
         IoOveruseStatsSubject.assertWithMessage(
                 "Fetched stats only for 4 days. Expected stats (%s) equals actual stats (%s)",
@@ -272,12 +266,11 @@
          * Returned stats shouldn't include stats for the current date as WatchdogPerfHandler fills
          * the current day's stats.
          */
-        ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
-                .truncatedTo(STATS_TEMPORAL_UNIT);
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
         long startTime = currentDate.minus(4, STATS_TEMPORAL_UNIT).toEpochSecond();
         long duration = currentDate.toEpochSecond() - startTime;
         IoOveruseStats expected = new IoOveruseStats.Builder(startTime, duration)
-                .setTotalOveruses(4).setTotalTimesKilled(2).setTotalBytesWritten(12_000).build();
+                .setTotalOveruses(4).setTotalTimesKilled(2).setTotalBytesWritten(12_600).build();
 
         IoOveruseStatsSubject.assertWithMessage(
                 "Fetched stats only for 2 days. Expected stats (%s) equals actual stats (%s)",
@@ -286,6 +279,120 @@
     }
 
     @Test
+    public void testGetDailySystemIoUsageSummaries() throws Exception {
+        injectSampleUserPackageSettings();
+        List<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
+        for (int i = 1; i <= 30; ++i) {
+            entries.addAll(sampleStatsBetweenDates(/* includingStartDaysAgo= */ i,
+                    /* excludingEndDaysAgo= */ i + 1, /* writtenBytesMultiplier= */ i));
+        }
+
+        assertWithMessage("Save I/O usage stats").that(mService.saveIoUsageStats(entries)).isTrue();
+
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
+
+        List<AtomsProto.CarWatchdogDailyIoUsageSummary> actual =
+                mService.getDailySystemIoUsageSummaries(
+                        /* includingStartEpochSeconds= */ currentDate.minusDays(15).toEpochSecond(),
+                        /* excludingEndEpochSeconds= */ currentDate.minusDays(7).toEpochSecond());
+
+        List<AtomsProto.CarWatchdogDailyIoUsageSummary> expected = new ArrayList<>();
+        for (int i = 15; i > 7; --i) {
+            expected.add(CarWatchdogServiceUnitTest
+                    .constructCarWatchdogDailyIoUsageSummary(/* fgWrBytes= */ 10402L * i,
+                            /* bgWrBytes= */ 14402L * i, /* gmWrBytes= */ 18402L * i,
+                            /* overuseCount= */ 6));
+        }
+
+        assertWithMessage("Daily system I/O usage summary stats").that(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testGetDailySystemIoUsageSummariesWithoutStats() throws Exception {
+        injectSampleUserPackageSettings();
+        List<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
+        for (int i = 1; i <= 7; ++i) {
+            entries.addAll(sampleStatsBetweenDates(/* includingStartDaysAgo= */ i,
+                    /* excludingEndDaysAgo= */ i + 1, /* writtenBytesMultiplier= */ i));
+        }
+
+        assertWithMessage("Save I/O usage stats").that(mService.saveIoUsageStats(entries)).isTrue();
+
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
+
+        List<AtomsProto.CarWatchdogDailyIoUsageSummary> actual =
+                mService.getDailySystemIoUsageSummaries(
+                        /* includingStartEpochSeconds= */ currentDate.minusDays(15).toEpochSecond(),
+                        /* excludingEndEpochSeconds= */ currentDate.minusDays(7).toEpochSecond());
+
+        assertWithMessage("Daily system I/O usage summary stats").that(actual).isNull();
+    }
+
+    @Test
+    public void testGetTopUsersDailyIoUsageSummaries() throws Exception {
+        injectSampleUserPackageSettings();
+        List<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
+        for (int i = 1; i <= 30; ++i) {
+            entries.addAll(sampleStatsBetweenDates(/* includingStartDaysAgo= */ i,
+                    /* excludingEndDaysAgo= */ i + 1, /* writtenBytesMultiplier= */ i));
+        }
+
+        assertWithMessage("Save I/O usage stats").that(mService.saveIoUsageStats(entries)).isTrue();
+
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
+
+        List<WatchdogStorage.UserPackageDailySummaries> actual =
+                mService.getTopUsersDailyIoUsageSummaries(/* numTopUsers= */ 3,
+                        /* minTotalWrittenBytes= */ 600_000,
+                        /* includingStartEpochSeconds= */ currentDate.minusDays(15).toEpochSecond(),
+                        /* excludingEndEpochSeconds= */ currentDate.minusDays(7).toEpochSecond());
+
+        List<AtomsProto.CarWatchdogDailyIoUsageSummary> user101VendorPkgSummaries =
+                new ArrayList<>();
+        List<AtomsProto.CarWatchdogDailyIoUsageSummary> user100VendorPkgSummaries =
+                new ArrayList<>();
+        for (int i = 15; i > 7; --i) {
+            user101VendorPkgSummaries.add(CarWatchdogServiceUnitTest
+                    .constructCarWatchdogDailyIoUsageSummary(/* fgWrBytes= */ 4101L * i,
+                            /* bgWrBytes= */ 5101L * i, /* gmWrBytes= */ 6101L * i,
+                            /* overuseCount= */ 1));
+            user100VendorPkgSummaries.add(CarWatchdogServiceUnitTest
+                    .constructCarWatchdogDailyIoUsageSummary(/* fgWrBytes= */ 4100L * i,
+                            /* bgWrBytes= */ 5100L * i, /* gmWrBytes= */ 6100L * i,
+                            /* overuseCount= */ 1));
+        }
+        List<WatchdogStorage.UserPackageDailySummaries> expected = Arrays.asList(
+                new WatchdogStorage.UserPackageDailySummaries(/* userId= */ 101,
+                        /* packageName= */ "vendor_package.critical.C", user101VendorPkgSummaries),
+                new WatchdogStorage.UserPackageDailySummaries(/* userId= */ 100,
+                        /* packageName= */ "vendor_package.critical.C", user100VendorPkgSummaries));
+
+        assertWithMessage("Top users daily I/O usage summaries").that(actual).isEqualTo(expected);
+    }
+
+    @Test
+    public void testGetTopUsersDailyIoUsageSummariesWithoutStats() throws Exception {
+        injectSampleUserPackageSettings();
+        List<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
+        for (int i = 1; i <= 7; ++i) {
+            entries.addAll(sampleStatsBetweenDates(/* includingStartDaysAgo= */ i,
+                    /* excludingEndDaysAgo= */ i + 1, /* writtenBytesMultiplier= */ i));
+        }
+
+        assertWithMessage("Save I/O usage stats").that(mService.saveIoUsageStats(entries)).isTrue();
+
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
+
+        List<WatchdogStorage.UserPackageDailySummaries> actual =
+                mService.getTopUsersDailyIoUsageSummaries(/* numTopUsers= */ 3,
+                        /* minTotalWrittenBytes= */ 600_000,
+                        /* includingStartEpochSeconds= */ currentDate.minusDays(15).toEpochSecond(),
+                        /* excludingEndEpochSeconds= */ currentDate.minusDays(7).toEpochSecond());
+
+        assertWithMessage("Top users daily I/O usage summaries").that(actual).isNull();
+    }
+
+    @Test
     public void testDeleteUserPackage() throws Exception {
         ArrayList<WatchdogStorage.UserPackageSettingsEntry> settingsEntries = sampleSettings();
         List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = sampleStatsForToday();
@@ -301,8 +408,8 @@
         settingsEntries.removeIf(
                 (s) -> s.userId == deleteUserId && s.packageName.equals(deletePackageName));
 
-        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
-                .containsExactlyElementsIn(settingsEntries);
+        assertWithMessage("User package settings after deleting a user package")
+                .that(mService.getUserPackageSettings()).containsExactlyElementsIn(settingsEntries);
 
         ioUsageStatsEntries.removeIf(
                 (e) -> e.userId == deleteUserId && e.packageName.equals(deletePackageName));
@@ -323,7 +430,7 @@
 
         mService.deleteUserPackage(deleteUserId, deletePackageName);
 
-        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+        assertWithMessage("User package settings").that(mService.getUserPackageSettings())
                 .containsExactlyElementsIn(sampleSettings());
 
         ioUsageStatsEntries.removeIf(
@@ -350,8 +457,8 @@
         settingsEntries.removeIf(
                 (s) -> s.userId == deleteUserId && s.packageName.equals(deletePackageName));
 
-        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
-                .containsExactlyElementsIn(settingsEntries);
+        assertWithMessage("User package settings after deleting user package with historical stats")
+                .that(mService.getUserPackageSettings()).containsExactlyElementsIn(settingsEntries);
 
         IoOveruseStats actual = mService.getHistoricalIoOveruseStats(
                 /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
@@ -372,8 +479,8 @@
         settingsEntries.removeIf((s) -> s.userId == 100);
         ioUsageStatsEntries.removeIf((e) -> e.userId == 100);
 
-        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
-                .containsExactlyElementsIn(settingsEntries);
+        assertWithMessage("User package settings after syncing alive users")
+                .that(mService.getUserPackageSettings()).containsExactlyElementsIn(settingsEntries);
 
         IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
                 .containsExactlyElementsIn(ioUsageStatsEntries);
@@ -391,8 +498,8 @@
 
         settingsEntries.removeIf((s) -> s.userId == 100);
 
-        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
-                .containsExactlyElementsIn(settingsEntries);
+        assertWithMessage("User package settings after syncing alive users with historical stats")
+                .that(mService.getUserPackageSettings()).containsExactlyElementsIn(settingsEntries);
 
         IoOveruseStats actualSystemPackage = mService.getHistoricalIoOveruseStats(
                 /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
@@ -415,8 +522,8 @@
 
         mService.syncUsers(/* aliveUserIds= */ new int[] {100, 101});
 
-        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
-                .containsExactlyElementsIn(settingsEntries);
+        assertWithMessage("User package settings after syncing users")
+                .that(mService.getUserPackageSettings()).containsExactlyElementsIn(settingsEntries);
         IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
                 .containsExactlyElementsIn(ioUsageStatsEntries);
     }
@@ -424,7 +531,7 @@
     @Test
     public void testTruncateStatsOutsideRetentionPeriodOnDateChange() throws Exception {
         injectSampleUserPackageSettings();
-        setDate(/* numDaysAgo= */ 1);
+        mTimeSource.updateNow(/* numDaysAgo= */ 1);
 
         assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
                 /* includingStartDaysAgo= */ 0, /* excludingEndDaysAgo= */ 40),
@@ -435,19 +542,18 @@
 
         assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
 
-        ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
-                .truncatedTo(STATS_TEMPORAL_UNIT);
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
         long startTime = currentDate.minus(39, STATS_TEMPORAL_UNIT).toEpochSecond();
         long duration = currentDate.toEpochSecond() - startTime;
         IoOveruseStats expected = new IoOveruseStats.Builder(startTime, duration)
-                .setTotalOveruses(78).setTotalTimesKilled(39).setTotalBytesWritten(234_000).build();
+                .setTotalOveruses(78).setTotalTimesKilled(39).setTotalBytesWritten(245_700).build();
 
         IoOveruseStatsSubject.assertWithMessage(
                 "Fetched stats only for 39 days. Expected stats (%s) equals actual stats (%s)",
                 expected.toString(), actual.toString()).that(actual)
                 .isEqualTo(expected);
 
-        setDate(/* numDaysAgo= */ 0);
+        mTimeSource.updateNow(/* numDaysAgo= */ 0);
         mService.shrinkDatabase();
 
         actual = mService.getHistoricalIoOveruseStats(
@@ -455,36 +561,17 @@
 
         assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
 
-        currentDate = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+        currentDate = mTimeSource.getCurrentDate();
         startTime = currentDate.minus(RETENTION_PERIOD.minusDays(1)).toEpochSecond();
         duration = currentDate.toEpochSecond() - startTime;
         expected = new IoOveruseStats.Builder(startTime, duration)
-                .setTotalOveruses(58).setTotalTimesKilled(29).setTotalBytesWritten(174_000).build();
+                .setTotalOveruses(58).setTotalTimesKilled(29).setTotalBytesWritten(182_700).build();
 
         IoOveruseStatsSubject.assertWithMessage("Fetched stats only within retention period. "
                         + "Expected stats (%s) equals actual stats (%s)",
                 expected.toString(), actual.toString()).that(actual).isEqualTo(expected);
     }
 
-    private void setDate(int numDaysAgo) {
-        TimeSourceInterface timeSource = new TimeSourceInterface() {
-            @Override
-            public Instant now() {
-                /* Return the same time, so the tests are deterministic. */
-                return mNow;
-            }
-
-            @Override
-            public String toString() {
-                return "Mocked date to " + now();
-            }
-
-            private final Instant mNow = Instant.now().minus(numDaysAgo, ChronoUnit.DAYS);
-        };
-        mService.setTimeSource(timeSource);
-        mTimeSource = timeSource;
-    }
-
     private void injectSampleUserPackageSettings() throws Exception {
         List<WatchdogStorage.UserPackageSettingsEntry> expected = sampleSettings();
 
@@ -509,45 +596,60 @@
 
     private ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsBetweenDates(
             int includingStartDaysAgo, int excludingEndDaysAgo) {
-        ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
-                .truncatedTo(STATS_TEMPORAL_UNIT);
+        return sampleStatsBetweenDates(includingStartDaysAgo, excludingEndDaysAgo,
+                /* writtenBytesMultiplier= */ 1);
+    }
+
+    private ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsBetweenDates(
+            int includingStartDaysAgo, int excludingEndDaysAgo, int writtenBytesMultiplier) {
+        ZonedDateTime currentDate = mTimeSource.getCurrentDate();
         ArrayList<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
         for (int i = includingStartDaysAgo; i < excludingEndDaysAgo; ++i) {
-            entries.addAll(sampleStatsForDate(
-                    currentDate.minus(i, STATS_TEMPORAL_UNIT).toEpochSecond(),
-                    STATS_TEMPORAL_UNIT.getDuration().toSeconds()));
+            entries.addAll(
+                    sampleStatsForDate(currentDate.minus(i, STATS_TEMPORAL_UNIT).toEpochSecond(),
+                            STATS_TEMPORAL_UNIT.getDuration().toSeconds(), writtenBytesMultiplier));
         }
         return entries;
     }
 
     private ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsForToday() {
-        long currentTime = mTimeSource.now().atZone(ZONE_OFFSET)
-                .truncatedTo(STATS_TEMPORAL_UNIT).toEpochSecond();
-        long duration = mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - currentTime;
-        return sampleStatsForDate(currentTime, duration);
+        long statsDateEpoch = mTimeSource.getCurrentDate().toEpochSecond();
+        long duration = mTimeSource.getCurrentDateTime().toEpochSecond() - statsDateEpoch;
+        return sampleStatsForDate(statsDateEpoch, duration, /* writtenBytesMultiplier= */ 1);
     }
 
     private static ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsForDate(
             long statsDateEpoch, long duration) {
+        return sampleStatsForDate(statsDateEpoch, duration, /* writtenBytesMultiplier= */ 1);
+    }
+
+    private static ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsForDate(
+            long statsDateEpoch, long duration, int writtenBytesMultiplier) {
         ArrayList<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
         for (int i = 100; i <= 101; ++i) {
             entries.add(constructIoUsageStatsEntry(
                     /* userId= */ i, "system_package.non_critical.A", statsDateEpoch, duration,
                     /* remainingWriteBytes= */
-                    CarWatchdogServiceUnitTest.constructPerStateBytes(200, 300, 400),
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(200L, 300L, 400L),
                     /* writtenBytes= */
-                    CarWatchdogServiceUnitTest.constructPerStateBytes(1000, 2000, 3000),
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(
+                            (1000L + i) * writtenBytesMultiplier,
+                            (2000L + i) * writtenBytesMultiplier,
+                            (3000L + i) * writtenBytesMultiplier),
                     /* forgivenWriteBytes= */
-                    CarWatchdogServiceUnitTest.constructPerStateBytes(100, 100, 100),
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(100L, 100L, 100L),
                     /* totalOveruses= */ 2, /* totalTimesKilled= */ 1));
             entries.add(constructIoUsageStatsEntry(
                     /* userId= */ i, "vendor_package.critical.C", statsDateEpoch, duration,
                     /* remainingWriteBytes= */
-                    CarWatchdogServiceUnitTest.constructPerStateBytes(500, 600, 700),
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(500L, 600L, 700L),
                     /* writtenBytes= */
-                    CarWatchdogServiceUnitTest.constructPerStateBytes(4000, 5000, 6000),
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(
+                            (4000L + i) * writtenBytesMultiplier,
+                            (5000L + i) * writtenBytesMultiplier,
+                            (6000L + i) * writtenBytesMultiplier),
                     /* forgivenWriteBytes= */
-                    CarWatchdogServiceUnitTest.constructPerStateBytes(200, 200, 200),
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(200L, 200L, 200L),
                     /* totalOveruses= */ 1, /* totalTimesKilled= */ 0));
         }
         return entries;
@@ -575,4 +677,27 @@
         stats.totalOveruses = totalOveruses;
         return stats;
     }
+
+    private static final class TestTimeSource extends TimeSource {
+        private static final Instant TEST_DATE_TIME = Instant.parse("2021-11-12T13:14:15.16Z");
+        private Instant mNow;
+        TestTimeSource() {
+            mNow = TEST_DATE_TIME;
+        }
+
+        @Override
+        public Instant now() {
+            /* Return the same time, so the tests are deterministic. */
+            return mNow;
+        }
+
+        @Override
+        public String toString() {
+            return "Mocked date to " + now();
+        }
+
+        void updateNow(int numDaysAgo) {
+            mNow = TEST_DATE_TIME.minus(numDaysAgo, ChronoUnit.DAYS);
+        }
+    };
 }