Merge "Ignore min/max SampleRate for static properties" into sc-v2-dev
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1b105cc..926a882 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -12,6 +12,9 @@
           "include-filter": "android.car.apitest.PreInstalledPackagesTest"
         }
       ]
+    },
+    {
+      "name": "SampleCustomInputServiceTest"
     }
   ]
 }
\ No newline at end of file
diff --git a/car-lib/src/android/car/input/CarInputManager.java b/car-lib/src/android/car/input/CarInputManager.java
index 8199bd0..7eddcf8 100644
--- a/car-lib/src/android/car/input/CarInputManager.java
+++ b/car-lib/src/android/car/input/CarInputManager.java
@@ -264,8 +264,10 @@
      * same {@link CarInputManager} instance, then only the last registered callback will receive
      * events, even if they were registered for different input event types.
      *
-     * @throws SecurityException is caller doesn't have android.car.permission.CAR_MONITOR_INPUT
-     *                           permission granted
+     * @throws SecurityException if caller doesn't have
+     *                           {@code android.car.permission.CAR_MONITOR_INPUT} permission
+     *                           granted. Currently this method also accept
+     *                           {@code android.permission.MONITOR_INPUT}
      * @throws IllegalArgumentException if targetDisplayType parameter correspond to a non supported
      *                                  display type
      * @throws IllegalArgumentException if inputTypes parameter contains invalid or non supported
@@ -292,6 +294,10 @@
      * CarInputCaptureCallback)} except that callbacks are invoked using
      * the executor passed as parameter.
      *
+     * @throws SecurityException if caller doesn't have
+     *                           {@code android.permission.MONITOR_INPUT} permission
+     *                           granted. Currently this method also accept
+     *                           {@code android.car.permission.CAR_MONITOR_INPUT}
      * @param targetDisplayType the display type to register callback against
      * @param inputTypes the input type to register callback against
      * @param requestFlags the capture request flag
diff --git a/car-lib/src/android/car/telemetry/CarTelemetryManager.java b/car-lib/src/android/car/telemetry/CarTelemetryManager.java
index 3d3cf50..3c4c187 100644
--- a/car-lib/src/android/car/telemetry/CarTelemetryManager.java
+++ b/car-lib/src/android/car/telemetry/CarTelemetryManager.java
@@ -45,7 +45,7 @@
 
     private static final boolean DEBUG = false;
     private static final String TAG = CarTelemetryManager.class.getSimpleName();
-    private static final int MANIFEST_MAX_SIZE_BYTES = 10 * 1024; // 10 kb
+    private static final int METRICS_CONFIG_MAX_SIZE_BYTES = 10 * 1024; // 10 kb
 
     private final CarTelemetryServiceListener mCarTelemetryServiceListener =
             new CarTelemetryServiceListener(this);
@@ -58,49 +58,51 @@
     private Executor mExecutor;
 
     /**
-     * Status to indicate that manifest was added successfully.
+     * Status to indicate that MetricsConfig was added successfully.
      */
-    public static final int ERROR_NONE = 0;
+    public static final int ERROR_METRICS_CONFIG_NONE = 0;
 
     /**
-     * Status to indicate that add manifest failed because the same manifest based on the
+     * Status to indicate that add MetricsConfig failed because the same MetricsConfig based on the
      * ManifestKey already exists.
      */
-    public static final int ERROR_SAME_MANIFEST_EXISTS = 1;
+    public static final int ERROR_METRICS_CONFIG_ALREADY_EXISTS = 1;
 
     /**
-     * Status to indicate that add manifest failed because a newer version of the manifest exists.
+     * Status to indicate that add MetricsConfig failed because a newer version of the MetricsConfig
+     * exists.
      */
-    public static final int ERROR_NEWER_MANIFEST_EXISTS = 2;
+    public static final int ERROR_METRICS_CONFIG_VERSION_TOO_OLD = 2;
 
     /**
-     * Status to indicate that add manifest failed because CarTelemetryService is unable to parse
-     * the given byte array into a Manifest.
+     * Status to indicate that add MetricsConfig failed because CarTelemetryService is unable to
+     * parse the given byte array into a MetricsConfig.
      */
-    public static final int ERROR_PARSE_MANIFEST_FAILED = 3;
+    public static final int ERROR_METRICS_CONFIG_PARSE_FAILED = 3;
 
     /**
-     * Status to indicate that add manifest failed because of failure to verify the signature of
-     * the manifest.
+     * Status to indicate that add MetricsConfig failed because of failure to verify the signature
+     * of the MetricsConfig.
      */
-    public static final int ERROR_SIGNATURE_VERIFICATION_FAILED = 4;
+    public static final int ERROR_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED = 4;
 
     /**
-     * Status to indicate that add manifest failed because of a general error in cars.
+     * Status to indicate that add MetricsConfig failed because of a general error in cars.
      */
-    public static final int ERROR_UNKNOWN = 5;
+    public static final int ERROR_METRICS_CONFIG_UNKNOWN = 5;
 
     /** @hide */
-    @IntDef(prefix = {"ERROR_"}, value = {
-            ERROR_NONE,
-            ERROR_SAME_MANIFEST_EXISTS,
-            ERROR_NEWER_MANIFEST_EXISTS,
-            ERROR_PARSE_MANIFEST_FAILED,
-            ERROR_SIGNATURE_VERIFICATION_FAILED,
-            ERROR_UNKNOWN
+    @IntDef(prefix = {"ERROR_METRICS_CONFIG_"}, value = {
+            ERROR_METRICS_CONFIG_NONE,
+            ERROR_METRICS_CONFIG_ALREADY_EXISTS,
+            ERROR_METRICS_CONFIG_VERSION_TOO_OLD,
+            ERROR_METRICS_CONFIG_PARSE_FAILED,
+            ERROR_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED,
+            ERROR_METRICS_CONFIG_UNKNOWN
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface AddManifestError {}
+    public @interface MetricsConfigError {
+    }
 
     /**
      * Application registers {@link CarTelemetryResultsListener} object to receive data from
@@ -110,22 +112,39 @@
      */
     public interface CarTelemetryResultsListener {
         /**
-         * Called by {@link com.android.car.telemetry.CarTelemetryService} to send script result to
-         * the client.
+         * Sends script results to the client. Called by {@link CarTelemetryServiceListener}.
+         *
          * TODO(b/184964661): Publish the documentation for the format of the results.
          *
-         * @param key the {@link ManifestKey} that the result is associated with.
-         * @param result the serialized car telemetry result.
+         * @param key    the {@link MetricsConfigKey} that the result is associated with.
+         * @param result the car telemetry result as serialized bytes.
          */
-        void onResult(@NonNull ManifestKey key, @NonNull byte[] result);
+        void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result);
 
         /**
-         * Called by {@link com.android.car.telemetry.CarTelemetryService} to send error message to
-         * the client.
+         * Sends script execution errors to the client.
          *
+         * @param key   the {@link MetricsConfigKey} that the error is associated with
          * @param error the serialized car telemetry error.
          */
-        void onError(@NonNull byte[] error);
+        void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error);
+
+        /**
+         * Sends the {@link #addMetricsConfig(MetricsConfigKey, byte[])} status to the client.
+         *
+         * @param key        the {@link MetricsConfigKey} that the status is associated with
+         * @param statusCode See {@link MetricsConfigError}.
+         */
+        void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key,
+                @MetricsConfigError int statusCode);
+
+        /**
+         * Sends the {@link #removeMetricsConfig(MetricsConfigKey)} status to the client.
+         *
+         * @param key     the {@link MetricsConfigKey} that the status is associated with
+         * @param success true for successful removal, false otherwise.
+         */
+        void onRemoveMetricsConfigStatus(@NonNull MetricsConfigKey key, boolean success);
     }
 
     /**
@@ -141,7 +160,7 @@
         }
 
         @Override
-        public void onResult(@NonNull ManifestKey key, @NonNull byte[] result) {
+        public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
             CarTelemetryManager manager = mManager.get();
             if (manager == null) {
                 return;
@@ -150,27 +169,66 @@
         }
 
         @Override
-        public void onError(@NonNull byte[] error) {
+        public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
             CarTelemetryManager manager = mManager.get();
             if (manager == null) {
                 return;
             }
-            manager.onError(error);
+            manager.onError(key, error);
+        }
+
+        @Override
+        public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key,
+                @MetricsConfigError int statusCode) {
+            CarTelemetryManager manager = mManager.get();
+            if (manager == null) {
+                return;
+            }
+            manager.onAddMetricsConfigStatus(key, statusCode);
+        }
+
+        @Override
+        public void onRemoveMetricsConfigStatus(@NonNull MetricsConfigKey key, boolean success) {
+            CarTelemetryManager manager = mManager.get();
+            if (manager == null) {
+                return;
+            }
+            manager.onRemoveMetricsConfigStatus(key, success);
         }
     }
 
-    private void onResult(ManifestKey key, byte[] result) {
+    private void onResult(MetricsConfigKey key, byte[] result) {
         long token = Binder.clearCallingIdentity();
         synchronized (mLock) {
+            // TODO(b/198824696): listener should be nonnull
             mExecutor.execute(() -> mResultsListener.onResult(key, result));
         }
         Binder.restoreCallingIdentity(token);
     }
 
-    private void onError(byte[] error) {
+    private void onError(MetricsConfigKey key, byte[] error) {
         long token = Binder.clearCallingIdentity();
         synchronized (mLock) {
-            mExecutor.execute(() -> mResultsListener.onError(error));
+            // TODO(b/198824696): listener should be nonnull
+            mExecutor.execute(() -> mResultsListener.onError(key, error));
+        }
+        Binder.restoreCallingIdentity(token);
+    }
+
+    private void onAddMetricsConfigStatus(MetricsConfigKey key, int statusCode) {
+        long token = Binder.clearCallingIdentity();
+        synchronized (mLock) {
+            // TODO(b/198824696): listener should be nonnull
+            mExecutor.execute(() -> mResultsListener.onAddMetricsConfigStatus(key, statusCode));
+        }
+        Binder.restoreCallingIdentity(token);
+    }
+
+    private void onRemoveMetricsConfigStatus(MetricsConfigKey key, boolean success) {
+        long token = Binder.clearCallingIdentity();
+        synchronized (mLock) {
+            // TODO(b/198824696): listener should be nonnull
+            mExecutor.execute(() -> mResultsListener.onRemoveMetricsConfigStatus(key, success));
         }
         Binder.restoreCallingIdentity(token);
     }
@@ -209,7 +267,6 @@
      *
      * @param listener to received data from {@link com.android.car.telemetry.CarTelemetryService}.
      * @throws IllegalStateException if the listener is already set.
-     *
      * @hide
      */
     @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
@@ -249,77 +306,75 @@
     }
 
     /**
-     * Called by client to send telemetry manifest. The size of the manifest cannot exceed a
-     * predefined size. Otherwise an exception is thrown.
-     * The {@link ManifestKey} is used to uniquely identify a manifest. If a manifest of the same
-     * name already exists in {@link com.android.car.telemetry.CarTelemetryService}, then the
-     * version will be compared. If the version is strictly higher, the existing manifest will be
-     * replaced by the new one. All cache and intermediate results will be cleared if replaced.
-     * TODO(b/185420981): Update javadoc after CarTelemetryService has concrete implementation.
+     * Sends a telemetry MetricsConfig to CarTelemetryService. The size of the MetricsConfig cannot
+     * exceed a predefined size, otherwise an exception is thrown.
+     * The {@link MetricsConfigKey} is used to uniquely identify a MetricsConfig. If a MetricsConfig
+     * of the same name already exists in {@link com.android.car.telemetry.CarTelemetryService},
+     * the config version will be compared. If the version is strictly higher, the existing
+     * MetricsConfig will be replaced by the new one. All cache and intermediate results will be
+     * cleared if replaced.
+     * The status of this API is sent back asynchronously via {@link CarTelemetryResultsListener}.
      *
-     * @param key      the unique key to identify the manifest.
-     * @param manifest the serialized bytes of a Manifest object.
-     * @return {@link #AddManifestError} to tell the result of the request.
-     * @throws IllegalArgumentException if the manifest size exceeds limit.
-     *
+     * @param key           the unique key to identify the MetricsConfig.
+     * @param metricsConfig the serialized bytes of a MetricsConfig object.
+     * @throws IllegalArgumentException if the MetricsConfig size exceeds limit.
      * @hide
      */
     @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
-    public @AddManifestError int addManifest(@NonNull ManifestKey key, @NonNull byte[] manifest) {
-        if (manifest.length > MANIFEST_MAX_SIZE_BYTES) {
-            throw new IllegalArgumentException("Manifest size exceeds limit.");
+    public void addMetricsConfig(@NonNull MetricsConfigKey key, @NonNull byte[] metricsConfig) {
+        if (metricsConfig.length > METRICS_CONFIG_MAX_SIZE_BYTES) {
+            throw new IllegalArgumentException("MetricsConfig size exceeds limit.");
         }
         try {
-            return mService.addManifest(key, manifest);
+            mService.addMetricsConfig(key, metricsConfig);
         } catch (RemoteException e) {
             handleRemoteExceptionFromCarService(e);
         }
-        return ERROR_UNKNOWN;
     }
 
     /**
-     * Removes a manifest from {@link com.android.car.telemetry.CarTelemetryService}. If the
-     * manifest does not exist, nothing will be removed but the status will be indicated in the
-     * return value.
+     * Removes a MetricsConfig from {@link com.android.car.telemetry.CarTelemetryService}. If the
+     * MetricsConfig does not exist, nothing will be removed.
+     * The status of this API is sent back asynchronously via {@link CarTelemetryResultsListener}.
      *
-     * @param key the unique key to identify the manifest. Name and version must be exact.
+     * @param key the unique key to identify the MetricsConfig. Name and version must be exact.
      * @return true for success, false otherwise.
      * @hide
      */
     @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
-    public boolean removeManifest(@NonNull ManifestKey key) {
+    public void removeMetricsConfig(@NonNull MetricsConfigKey key) {
         try {
-            return mService.removeManifest(key);
+            mService.removeMetricsConfig(key);
         } catch (RemoteException e) {
             handleRemoteExceptionFromCarService(e);
         }
-        return false;
     }
 
     /**
-     * Removes all manifests from {@link com.android.car.telemetry.CarTelemetryService}.
+     * Removes all MetricsConfigs from {@link com.android.car.telemetry.CarTelemetryService}.
      *
      * @hide
      */
     @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
-    public void removeAllManifests() {
+    public void removeAllMetricsConfigs() {
         try {
-            mService.removeAllManifests();
+            mService.removeAllMetricsConfigs();
         } catch (RemoteException e) {
             handleRemoteExceptionFromCarService(e);
         }
     }
 
     /**
-     * An asynchronous API for the client to get script execution results of a specific manifest
-     * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
+     * Gets script execution results of a MetricsConfig as from the
+     * {@link com.android.car.telemetry.CarTelemetryService}. This API is asynchronous and the
+     * result is sent back asynchronously via the {@link CarTelemetryResultsListener}.
      * This call is destructive. The returned results will be deleted from CarTelemetryService.
      *
-     * @param key the unique key to identify the manifest.
+     * @param key the unique key to identify the MetricsConfig.
      * @hide
      */
     @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
-    public void sendFinishedReports(@NonNull ManifestKey key) {
+    public void sendFinishedReports(@NonNull MetricsConfigKey key) {
         try {
             mService.sendFinishedReports(key);
         } catch (RemoteException e) {
@@ -328,8 +383,8 @@
     }
 
     /**
-     * An asynchronous API for the client to get all script execution results
-     * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
+     * Gets all script execution results from {@link com.android.car.telemetry.CarTelemetryService}
+     * asynchronously via the {@link CarTelemetryResultsListener}.
      * This call is destructive. The returned results will be deleted from CarTelemetryService.
      *
      * @hide
@@ -342,20 +397,4 @@
             handleRemoteExceptionFromCarService(e);
         }
     }
-
-    /**
-     * An asynchronous API for the client to get all script execution errors
-     * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
-     * This call is destructive. The returned results will be deleted from CarTelemetryService.
-     *
-     * @hide
-     */
-    @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
-    public void sendScriptExecutionErrors() {
-        try {
-            mService.sendScriptExecutionErrors();
-        } catch (RemoteException e) {
-            handleRemoteExceptionFromCarService(e);
-        }
-    }
 }
diff --git a/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl b/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
index 09743d8..49472f4 100644
--- a/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
+++ b/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
@@ -1,7 +1,7 @@
 package android.car.telemetry;
 
 import android.car.telemetry.ICarTelemetryServiceListener;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.MetricsConfigKey;
 
 /**
  * Internal binder interface for {@code CarTelemetryService}, used by {@code CarTelemetryManager}.
@@ -21,33 +21,28 @@
     void clearListener();
 
     /**
-     * Sends telemetry manifests to CarTelemetryService.
+     * Sends telemetry MetricsConfigs to CarTelemetryService.
      */
-    int addManifest(in ManifestKey key, in byte[] manifest);
+    void addMetricsConfig(in MetricsConfigKey key, in byte[] metricsConfig);
 
     /**
-     * Removes a manifest based on the key.
+     * Removes a MetricsConfig based on the key.
      */
-    boolean removeManifest(in ManifestKey key);
+    void removeMetricsConfig(in MetricsConfigKey key);
 
     /**
-     * Removes all manifests.
+     * Removes all MetricsConfigs.
      */
-    void removeAllManifests();
+    void removeAllMetricsConfigs();
 
     /**
-     * Sends script results associated with the given key using the
+     * Sends script results or errors associated with the given key using the
      * {@code ICarTelemetryServiceListener}.
      */
-    void sendFinishedReports(in ManifestKey key);
+    void sendFinishedReports(in MetricsConfigKey key);
 
     /**
-     * Sends all script results associated using the {@code ICarTelemetryServiceListener}.
+     * Sends all script results or errors using the {@code ICarTelemetryServiceListener}.
      */
     void sendAllFinishedReports();
-
-    /**
-     * Sends all errors using the {@code ICarTelemetryServiceListener}.
-     */
-    void sendScriptExecutionErrors();
 }
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/ICarTelemetryServiceListener.aidl b/car-lib/src/android/car/telemetry/ICarTelemetryServiceListener.aidl
index ba4ca2d..4bd61fd 100644
--- a/car-lib/src/android/car/telemetry/ICarTelemetryServiceListener.aidl
+++ b/car-lib/src/android/car/telemetry/ICarTelemetryServiceListener.aidl
@@ -16,7 +16,7 @@
 
 package android.car.telemetry;
 
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.MetricsConfigKey;
 import java.util.List;
 
 /**
@@ -34,13 +34,29 @@
      * @param key the key that the result is associated with.
      * @param result the serialized bytes of the script execution result message.
      */
-    void onResult(in ManifestKey key, in byte[] result);
+    void onResult(in MetricsConfigKey key, in byte[] result);
 
     /**
-     * Called by {@code CarTelemetryService} to provide telemetry errors. This call is destrutive.
+     * Called by {@code CarTelemetryService} to provide telemetry errors. This call is destructive.
      * The parameter will no longer be stored in {@code CarTelemetryService}.
      *
      * @param error the serialized bytes of an error message.
      */
-    void onError(in byte[] error);
+    void onError(in MetricsConfigKey key, in byte[] error);
+
+    /**
+     * Sends the {@link #addMetricsConfig(MetricsConfigKey, byte[])} status to the client.
+     *
+     * @param key the {@link MetricsConfigKey} that the status is associated with.
+     * @param statusCode indicating add status.
+     */
+     void onAddMetricsConfigStatus(in MetricsConfigKey key, in int statusCode);
+
+    /**
+     * Sends the {@link #remove(MetricsConfigKey)} status to the client.
+     *
+     * @param key the {@link MetricsConfigKey} that the status is associated with.
+     * @param success true for successful removal, false otherwise.
+     */
+     void onRemoveMetricsConfigStatus(in MetricsConfigKey key, in boolean success);
 }
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.aidl b/car-lib/src/android/car/telemetry/MetricsConfigKey.aidl
similarity index 95%
rename from car-lib/src/android/car/telemetry/ManifestKey.aidl
rename to car-lib/src/android/car/telemetry/MetricsConfigKey.aidl
index 25097df..2c00127 100644
--- a/car-lib/src/android/car/telemetry/ManifestKey.aidl
+++ b/car-lib/src/android/car/telemetry/MetricsConfigKey.aidl
@@ -19,4 +19,4 @@
 /**
  * @hide
  */
-parcelable ManifestKey;
\ No newline at end of file
+parcelable MetricsConfigKey;
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.java b/car-lib/src/android/car/telemetry/MetricsConfigKey.java
similarity index 73%
rename from car-lib/src/android/car/telemetry/ManifestKey.java
rename to car-lib/src/android/car/telemetry/MetricsConfigKey.java
index b0a69c2..6e4614b 100644
--- a/car-lib/src/android/car/telemetry/ManifestKey.java
+++ b/car-lib/src/android/car/telemetry/MetricsConfigKey.java
@@ -25,7 +25,7 @@
  *
  * @hide
  */
-public final class ManifestKey implements Parcelable {
+public final class MetricsConfigKey implements Parcelable {
 
     @NonNull
     private String mName;
@@ -46,12 +46,12 @@
         out.writeInt(mVersion);
     }
 
-    private ManifestKey(Parcel in) {
+    private MetricsConfigKey(Parcel in) {
         mName = in.readString();
         mVersion = in.readInt();
     }
 
-    public ManifestKey(@NonNull String name, int version) {
+    public MetricsConfigKey(@NonNull String name, int version) {
         mName = name;
         mVersion = version;
     }
@@ -61,16 +61,16 @@
         return 0;
     }
 
-    public static final @NonNull Parcelable.Creator<ManifestKey> CREATOR =
-            new Parcelable.Creator<ManifestKey>() {
+    public static final @NonNull Parcelable.Creator<MetricsConfigKey> CREATOR =
+            new Parcelable.Creator<MetricsConfigKey>() {
                 @Override
-                public ManifestKey createFromParcel(Parcel in) {
-                    return new ManifestKey(in);
+                public MetricsConfigKey createFromParcel(Parcel in) {
+                    return new MetricsConfigKey(in);
                 }
 
                 @Override
-                public ManifestKey[] newArray(int size) {
-                    return new ManifestKey[size];
+                public MetricsConfigKey[] newArray(int size) {
+                    return new MetricsConfigKey[size];
                 }
             };
 }
diff --git a/car-test-lib/src/android/car/testapi/CarTelemetryController.java b/car-test-lib/src/android/car/testapi/CarTelemetryController.java
deleted file mode 100644
index e4df3f3..0000000
--- a/car-test-lib/src/android/car/testapi/CarTelemetryController.java
+++ /dev/null
@@ -1,48 +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 android.car.testapi;
-
-import android.car.telemetry.ManifestKey;
-
-/**
- * Controller to manipulate and verify {@link android.car.telemetry.CarTelemetryManager} in
- * unit tests.
- */
-public interface CarTelemetryController {
-    /**
-     * Returns {@code true} if a {@link
-     * android.car.telemetry.CarTelemetryManager.CarTelemetryResultsListener} is
-     * registered with the manager, otherwise returns {@code false}.
-     */
-    boolean isListenerSet();
-
-    /**
-     * Returns the number of valid manifests registered with the manager.
-     */
-    int getValidManifestsCount();
-
-    /**
-     * Associate a blob of data with the given key, used for testing the flush reports APIs.
-     */
-    void addDataForKey(ManifestKey key, byte[] data);
-
-    /**
-     * Configure the blob of data to be flushed with the
-     * {@code FakeCarTelemetryService#flushScriptExecutionErrors()} API.
-     */
-    void setErrorData(byte[] error);
-}
diff --git a/car-test-lib/src/android/car/testapi/FakeCar.java b/car-test-lib/src/android/car/testapi/FakeCar.java
index 0508ed0..0356774 100644
--- a/car-test-lib/src/android/car/testapi/FakeCar.java
+++ b/car-test-lib/src/android/car/testapi/FakeCar.java
@@ -127,14 +127,6 @@
         return mService.mCarUxRestrictionService;
     }
 
-    /**
-     * Returns a test controller that can modify and query the underlying service for the {@link
-     * android.car.telemetry.CarTelemetryManager}.
-     */
-    public CarTelemetryController getCarTelemetryController() {
-        return mService.mCarTelemetry;
-    }
-
     private static class FakeCarService extends ICar.Stub {
         @Mock ICarPackageManager.Stub mCarPackageManager;
         @Mock ICarDiagnostic.Stub mCarDiagnostic;
@@ -150,7 +142,6 @@
         private final FakeCarProjectionService mCarProjection;
         private final FakeInstrumentClusterNavigation mInstrumentClusterNavigation;
         private final FakeCarUxRestrictionsService mCarUxRestrictionService;
-        private final FakeCarTelemetryService mCarTelemetry;
 
         FakeCarService(Context context) {
             MockitoAnnotations.initMocks(this);
@@ -160,7 +151,6 @@
             mCarProjection = new FakeCarProjectionService(context);
             mInstrumentClusterNavigation = new FakeInstrumentClusterNavigation();
             mCarUxRestrictionService = new FakeCarUxRestrictionsService();
-            mCarTelemetry = new FakeCarTelemetryService();
         }
 
         @Override
@@ -203,8 +193,6 @@
                     return mCarDrivingState;
                 case Car.CAR_UX_RESTRICTION_SERVICE:
                     return mCarUxRestrictionService;
-                case Car.CAR_TELEMETRY_SERVICE:
-                    return mCarTelemetry;
                 default:
                     Log.w(TAG, "getCarService for unknown service:" + serviceName);
                     return null;
diff --git a/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java b/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java
deleted file mode 100644
index 4c6c912..0000000
--- a/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java
+++ /dev/null
@@ -1,128 +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 android.car.testapi;
-
-import static android.car.telemetry.CarTelemetryManager.ERROR_NEWER_MANIFEST_EXISTS;
-import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
-import static android.car.telemetry.CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS;
-
-import android.car.telemetry.CarTelemetryManager.AddManifestError;
-import android.car.telemetry.ICarTelemetryService;
-import android.car.telemetry.ICarTelemetryServiceListener;
-import android.car.telemetry.ManifestKey;
-import android.os.RemoteException;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A fake implementation of {@link ICarTelemetryService.Stub} to facilitate the use of
- * {@link android.car.telemetry.CarTelemetryManager} in external unit tests.
- *
- * @hide
- */
-public class FakeCarTelemetryService extends ICarTelemetryService.Stub implements
-        CarTelemetryController {
-
-    private byte[] mErrorBytes;
-    private ICarTelemetryServiceListener mListener;
-
-    private final Map<String, Integer> mNameVersionMap = new HashMap<>();
-    private final Map<ManifestKey, byte[]> mManifestMap = new HashMap<>();
-    private final Map<ManifestKey, byte[]> mScriptResultMap = new HashMap<>();
-
-    @Override
-    public void setListener(ICarTelemetryServiceListener listener) {
-        mListener = listener;
-    }
-
-    @Override
-    public void clearListener() {
-        mListener = null;
-    }
-
-    @Override
-    public @AddManifestError int addManifest(ManifestKey key, byte[] manifest) {
-        if (mNameVersionMap.getOrDefault(key.getName(), 0) > key.getVersion()) {
-            return ERROR_NEWER_MANIFEST_EXISTS;
-        } else if (mNameVersionMap.getOrDefault(key.getName(), 0) == key.getVersion()) {
-            return ERROR_SAME_MANIFEST_EXISTS;
-        }
-        mNameVersionMap.put(key.getName(), key.getVersion());
-        mManifestMap.put(key, manifest);
-        return ERROR_NONE;
-    }
-
-    @Override
-    public boolean removeManifest(ManifestKey key) {
-        if (!mManifestMap.containsKey(key)) {
-            return false;
-        }
-        mNameVersionMap.remove(key.getName());
-        mManifestMap.remove(key);
-        return true;
-    }
-
-    @Override
-    public void removeAllManifests() {
-        mNameVersionMap.clear();
-        mManifestMap.clear();
-    }
-
-    @Override
-    public void sendFinishedReports(ManifestKey key) throws RemoteException {
-        if (!mScriptResultMap.containsKey(key)) {
-            return;
-        }
-        mListener.onResult(key, mScriptResultMap.get(key));
-        mScriptResultMap.remove(key);
-    }
-
-    @Override
-    public void sendAllFinishedReports() throws RemoteException {
-        for (Map.Entry<ManifestKey, byte[]> entry : mScriptResultMap.entrySet()) {
-            mListener.onResult(entry.getKey(), entry.getValue());
-        }
-        mScriptResultMap.clear();
-    }
-
-    @Override
-    public void sendScriptExecutionErrors() throws RemoteException {
-        mListener.onError(mErrorBytes);
-    }
-
-    /**************************** CarTelemetryController impl ********************************/
-    @Override
-    public boolean isListenerSet() {
-        return mListener != null;
-    }
-
-    @Override
-    public int getValidManifestsCount() {
-        return mManifestMap.size();
-    }
-
-    @Override
-    public void addDataForKey(ManifestKey key, byte[] data) {
-        mScriptResultMap.put(key, data);
-    }
-
-    @Override
-    public void setErrorData(byte[] error) {
-        mErrorBytes = error;
-    }
-}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp
index c195412..01717c7 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp
@@ -12,6 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 android_app {
     name: "CarUiPortraitSystemUI",
 
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_background.xml
index d6efa1b..7104440 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_background.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_background.xml
@@ -17,7 +17,7 @@
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
     <item>
         <shape android:shape="rectangle">
-            <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+            <corners android:radius="@dimen/hvac_panel_seek_bar_radius"/>
             <solid android:color="@color/hvac_off_background_color" />
         </shape>
     </item>
@@ -27,13 +27,13 @@
         <selector>
             <item android:state_selected="true">
                 <shape android:shape="rectangle">
-                    <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+                    <corners android:radius="@dimen/hvac_panel_seek_bar_radius"/>
                     <solid android:color="@color/hvac_on_background_color" />
                 </shape>
             </item>
             <item>
                 <shape android:shape="rectangle">
-                    <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+                    <corners android:radius="@dimen/hvac_panel_seek_bar_radius"/>
                     <solid android:color="@color/hvac_off_background_color" />
                 </shape>
             </item>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_off.xml
deleted file mode 100644
index b6c1cb9..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_off.xml
+++ /dev/null
@@ -1,30 +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.
-  -->
-
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item>
-        <shape android:shape="oval">
-            <solid android:color="@color/hvac_off_background_color"/>
-            <size android:height="@dimen/hvac_panel_button_dimen"
-                  android:width="@dimen/hvac_panel_button_dimen"/>
-        </shape>
-    </item>
-    <item
-        android:drawable="@drawable/ic_mode_fan_off"
-        android:gravity="center"
-        android:width="@dimen/hvac_panel_icon_dimen"
-        android:height="@dimen/hvac_panel_icon_dimen"/>
-</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_off_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_off_bg.xml
index 2cc9886..d40ad01 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_off_bg.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_off_bg.xml
@@ -16,7 +16,7 @@
   -->
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
     <item>
-        <shape xmlns:android="http://schemas.android.com/apk/res/android">
+        <shape>
             <solid android:color="@color/hvac_off_background_color"/>
             <corners android:radius="@dimen/hvac_panel_off_button_radius"/>
         </shape>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_panel_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_panel_bg.xml
index 1bdac16..f0cd3bd 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_panel_bg.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_panel_bg.xml
@@ -17,5 +17,12 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
     <solid android:color="@color/hvac_background_color"/>
-    <corners android:radius="@dimen/hvac_panel_bg_radius"/>
+
+    <!-- android:radius must be defined even with overrides. -->
+    <corners
+        android:radius="1dp"
+        android:topLeftRadius="@dimen/hvac_panel_bg_radius"
+        android:topRightRadius="@dimen/hvac_panel_bg_radius"
+        android:bottomLeftRadius="0dp"
+        android:bottomRightRadius="0dp"/>
 </shape>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_high.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_high.xml
deleted file mode 100644
index afc41fd..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_high.xml
+++ /dev/null
@@ -1,40 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M9.708,0H11.522C12.022,0 12.5,0.177 12.854,0.492C14.146,1.646 14.324,3.461 13.276,4.797L12.949,5.213C11.271,7.354 11.081,10.155 12.459,12.461L17.528,20.946C19.855,24.842 19.399,29.596 16.362,33.081L12.495,37.52C12.136,37.932 11.404,37.79 11.268,37.284L4.898,13.59C3.834,9.63 4.369,5.463 6.408,1.821C7.033,0.705 8.311,0 9.708,0Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M25.871,36.811H38.065C41.142,36.811 43.636,39.037 43.636,41.784V43.027C43.636,45.773 41.142,48 38.065,48H22.607C20.021,48 17.542,47.083 15.714,45.451L14.161,44.065C13.552,43.521 13.636,42.62 14.336,42.174L20.205,38.432C21.858,37.378 23.839,36.811 25.871,36.811Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M39.701,13.057L37.221,13.945C37.026,13.804 36.809,13.674 36.592,13.577C36.777,12.677 37.275,11.865 38.066,11.345C40.254,9.893 39.528,6.167 36.506,6.167C31.75,6.167 29.757,9.427 31.056,12.298L31.945,14.779C31.804,14.974 31.674,15.191 31.576,15.408C30.677,15.223 29.865,14.725 29.345,13.934C27.893,11.746 24.167,12.472 24.167,15.494C24.167,20.261 27.427,22.254 30.298,20.943L32.779,20.055C32.974,20.196 33.191,20.326 33.407,20.423C33.223,21.323 32.725,22.135 31.934,22.655C29.746,24.107 30.472,27.833 33.494,27.833C38.261,27.833 40.254,24.573 38.943,21.702L38.055,19.221C38.196,19.026 38.326,18.809 38.423,18.593C39.322,18.777 40.135,19.275 40.655,20.066C42.106,22.243 45.822,21.517 45.822,18.495C45.833,13.75 42.572,11.757 39.701,13.057ZM35,18.625C34.101,18.625 33.375,17.899 33.375,17C33.375,16.101 34.101,15.375 35,15.375C35.899,15.375 36.625,16.101 36.625,17C36.625,17.899 35.899,18.625 35,18.625Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_low.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_low.xml
deleted file mode 100644
index 2888403..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_low.xml
+++ /dev/null
@@ -1,42 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M9.708,0H11.522C12.022,0 12.5,0.177 12.854,0.492C14.146,1.646 14.324,3.461 13.276,4.797L12.949,5.213C11.271,7.354 11.081,10.155 12.459,12.461L17.528,20.946C19.855,24.842 19.399,29.596 16.362,33.081L12.495,37.52C12.136,37.932 11.404,37.79 11.268,37.284L4.898,13.59C3.834,9.63 4.369,5.463 6.408,1.821C7.033,0.705 8.311,0 9.708,0Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M25.871,36.811H38.065C41.142,36.811 43.636,39.037 43.636,41.784V43.027C43.636,45.773 41.142,48 38.065,48H22.607C20.021,48 17.542,47.083 15.714,45.451L14.161,44.065C13.552,43.521 13.636,42.62 14.336,42.174L20.205,38.432C21.858,37.378 23.839,36.811 25.871,36.811Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M39.701,13.057L37.221,13.945C37.026,13.804 36.809,13.674 36.592,13.577C36.777,12.677 37.275,11.865 38.066,11.345C40.254,9.893 39.528,6.167 36.506,6.167C31.75,6.167 29.757,9.427 31.056,12.298L31.945,14.779C31.804,14.974 31.674,15.191 31.576,15.408C30.677,15.223 29.865,14.725 29.345,13.934C27.893,11.746 24.167,12.472 24.167,15.494C24.167,20.261 27.427,22.254 30.298,20.943L32.779,20.055C32.974,20.196 33.191,20.326 33.407,20.423C33.223,21.323 32.725,22.135 31.934,22.655C29.746,24.107 30.472,27.833 33.494,27.833C38.261,27.833 40.254,24.573 38.943,21.702L38.055,19.221C38.196,19.026 38.326,18.809 38.423,18.593C39.322,18.777 40.135,19.275 40.655,20.066C42.106,22.243 45.822,21.517 45.822,18.495C45.833,13.75 42.572,11.757 39.701,13.057ZM35,18.625C34.101,18.625 33.375,17.899 33.375,17C33.375,16.101 34.101,15.375 35,15.375C35.899,15.375 36.625,16.101 36.625,17C36.625,17.899 35.899,18.625 35,18.625Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_med.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_med.xml
deleted file mode 100644
index 98c5042..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_med.xml
+++ /dev/null
@@ -1,41 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M9.708,0H11.522C12.022,0 12.5,0.177 12.854,0.492C14.146,1.646 14.324,3.461 13.276,4.797L12.949,5.213C11.271,7.354 11.081,10.155 12.459,12.461L17.528,20.946C19.855,24.842 19.399,29.596 16.362,33.081L12.495,37.52C12.136,37.932 11.404,37.79 11.268,37.284L4.898,13.59C3.834,9.63 4.369,5.463 6.408,1.821C7.033,0.705 8.311,0 9.708,0Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M25.871,36.811H38.065C41.142,36.811 43.636,39.037 43.636,41.784V43.027C43.636,45.773 41.142,48 38.065,48H22.607C20.021,48 17.542,47.083 15.714,45.451L14.161,44.065C13.552,43.521 13.636,42.62 14.336,42.174L20.205,38.432C21.858,37.378 23.839,36.811 25.871,36.811Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M39.701,13.057L37.221,13.945C37.026,13.804 36.809,13.674 36.592,13.577C36.777,12.677 37.275,11.865 38.066,11.345C40.254,9.893 39.528,6.167 36.506,6.167C31.75,6.167 29.757,9.427 31.056,12.298L31.945,14.779C31.804,14.974 31.674,15.191 31.576,15.408C30.677,15.223 29.865,14.725 29.345,13.934C27.893,11.746 24.167,12.472 24.167,15.494C24.167,20.261 27.427,22.254 30.298,20.943L32.779,20.055C32.974,20.196 33.191,20.326 33.407,20.423C33.223,21.323 32.725,22.135 31.934,22.655C29.746,24.107 30.472,27.833 33.494,27.833C38.261,27.833 40.254,24.573 38.943,21.702L38.055,19.221C38.196,19.026 38.326,18.809 38.423,18.593C39.322,18.777 40.135,19.275 40.655,20.066C42.106,22.243 45.822,21.517 45.822,18.495C45.833,13.75 42.572,11.757 39.701,13.057ZM35,18.625C34.101,18.625 33.375,17.899 33.375,17C33.375,16.101 34.101,15.375 35,15.375C35.899,15.375 36.625,16.101 36.625,17C36.625,17.899 35.899,18.625 35,18.625Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_off.xml
deleted file mode 100644
index 5453dd5..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_cool_off.xml
+++ /dev/null
@@ -1,43 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_off_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_off_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_off_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M9.708,0H11.522C12.022,0 12.5,0.177 12.854,0.492C14.146,1.646 14.324,3.461 13.276,4.797L12.949,5.213C11.271,7.354 11.081,10.155 12.459,12.461L17.528,20.946C19.855,24.842 19.399,29.596 16.362,33.081L12.495,37.52C12.136,37.932 11.404,37.79 11.268,37.284L4.898,13.59C3.834,9.63 4.369,5.463 6.408,1.821C7.033,0.705 8.311,0 9.708,0Z"
-      android:fillColor="@color/hvac_off_icon_fill_color"/>
-  <path
-      android:pathData="M25.871,36.811H38.065C41.142,36.811 43.636,39.037 43.636,41.784V43.027C43.636,45.773 41.142,48 38.065,48H22.607C20.021,48 17.542,47.083 15.714,45.451L14.161,44.065C13.552,43.521 13.636,42.62 14.336,42.174L20.205,38.432C21.858,37.378 23.839,36.811 25.871,36.811Z"
-      android:fillColor="@color/hvac_off_icon_fill_color"/>
-  <path
-      android:pathData="M39.701,13.057L37.221,13.945C37.026,13.804 36.809,13.674 36.592,13.577C36.777,12.677 37.275,11.865 38.066,11.345C40.254,9.893 39.528,6.167 36.506,6.167C31.75,6.167 29.757,9.427 31.056,12.298L31.945,14.779C31.804,14.974 31.674,15.191 31.576,15.408C30.677,15.223 29.865,14.725 29.345,13.934C27.893,11.746 24.167,12.472 24.167,15.494C24.167,20.261 27.427,22.254 30.298,20.943L32.779,20.055C32.974,20.196 33.191,20.326 33.407,20.423C33.223,21.323 32.725,22.135 31.934,22.655C29.746,24.107 30.472,27.833 33.494,27.833C38.261,27.833 40.254,24.573 38.943,21.702L38.055,19.221C38.196,19.026 38.326,18.809 38.423,18.593C39.322,18.777 40.135,19.275 40.655,20.066C42.106,22.243 45.822,21.517 45.822,18.495C45.833,13.75 42.572,11.757 39.701,13.057ZM35,18.625C34.101,18.625 33.375,17.899 33.375,17C33.375,16.101 34.101,15.375 35,15.375C35.899,15.375 36.625,16.101 36.625,17C36.625,17.899 35.899,18.625 35,18.625Z"
-      android:fillColor="@color/hvac_off_icon_fill_color"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_high.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_high.xml
index 326b9b9..650fd9e 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_high.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_high.xml
@@ -15,21 +15,18 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
+        android:width="@dimen/hvac_panel_icon_dimen"
+        android:height="@dimen/hvac_panel_tall_icon_dimen"
+        android:viewportWidth="48"
+        android:viewportHeight="100">
   <path
       android:pathData="M11.522,0H9.708C8.311,0 7.033,0.705 6.408,1.821C4.369,5.463 3.834,9.63 4.898,13.59L11.268,37.284C11.404,37.79 12.136,37.932 12.495,37.52L16.362,33.081C19.399,29.596 19.855,24.842 17.528,20.946L12.459,12.461C11.081,10.155 11.271,7.354 12.949,5.213L13.276,4.797C14.324,3.461 14.146,1.646 12.854,0.492C12.5,0.177 12.022,0 11.522,0ZM38.065,36.811H25.871C23.839,36.811 21.858,37.378 20.205,38.432L14.336,42.174C13.636,42.62 13.552,43.521 14.161,44.065L15.714,45.451C17.542,47.083 20.021,48 22.607,48H38.065C41.142,48 43.636,45.773 43.636,43.027V41.784C43.636,39.037 41.142,36.811 38.065,36.811ZM39.06,13.841C38.405,14.32 38.089,14.748 37.933,15.092C37.782,15.428 37.739,15.787 37.799,16.216C37.933,17.181 38.537,18.271 39.372,19.674L39.506,19.899C40.228,21.107 41.121,22.603 41.377,24.089C41.525,24.942 41.481,25.861 41.069,26.767C40.66,27.663 39.952,28.411 39.001,29.04C38.31,29.497 37.38,29.307 36.923,28.616C36.466,27.925 36.656,26.994 37.347,26.537C37.96,26.132 38.221,25.78 38.339,25.523C38.451,25.276 38.488,24.99 38.421,24.599C38.265,23.692 37.648,22.643 36.794,21.209L36.775,21.176C36.019,19.906 35.059,18.293 34.827,16.63C34.703,15.738 34.778,14.795 35.197,13.862C35.613,12.938 36.319,12.129 37.289,11.42C37.957,10.931 38.896,11.076 39.385,11.745C39.874,12.414 39.729,13.352 39.06,13.841ZM31.797,15.092C31.952,14.748 32.269,14.32 32.924,13.841C33.592,13.352 33.738,12.414 33.248,11.745C32.759,11.076 31.821,10.931 31.152,11.42C30.183,12.129 29.476,12.938 29.061,13.862C28.641,14.795 28.566,15.738 28.691,16.63C28.923,18.293 29.883,19.906 30.639,21.176L30.658,21.209C31.511,22.643 32.128,23.692 32.285,24.599C32.352,24.99 32.315,25.276 32.202,25.523C32.085,25.78 31.823,26.132 31.211,26.537C30.52,26.994 30.33,27.925 30.787,28.616C31.243,29.307 32.174,29.497 32.865,29.04C33.816,28.411 34.524,27.663 34.932,26.767C35.345,25.861 35.388,24.942 35.241,24.089C34.985,22.603 34.092,21.107 33.37,19.899L33.236,19.674C32.401,18.271 31.797,17.181 31.662,16.216C31.602,15.787 31.646,15.428 31.797,15.092ZM26.787,13.841C26.132,14.32 25.816,14.748 25.661,15.092C25.51,15.428 25.466,15.787 25.526,16.216C25.66,17.181 26.264,18.271 27.1,19.674L27.234,19.899C27.955,21.107 28.848,22.603 29.105,24.089C29.252,24.942 29.208,25.861 28.796,26.767C28.388,27.663 27.68,28.411 26.729,29.04C26.038,29.497 25.107,29.307 24.65,28.616C24.193,27.925 24.383,26.994 25.074,26.537C25.687,26.132 25.949,25.78 26.066,25.523C26.178,25.276 26.216,24.99 26.148,24.599C25.992,23.692 25.375,22.643 24.522,21.209L24.502,21.176C23.747,19.906 22.786,18.293 22.555,16.63C22.43,15.738 22.505,14.795 22.925,13.862C23.34,12.938 24.046,12.129 25.016,11.42C25.684,10.931 26.623,11.076 27.112,11.745C27.601,12.414 27.456,13.352 26.787,13.841Z"
       android:fillColor="@color/hvac_on_icon_fill_color"
       android:fillType="evenOdd"/>
   <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
+      android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
       android:fillColor="@color/hvac_on_icon_fill_color"/>
   <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
+      android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
       android:fillColor="@color/hvac_on_icon_fill_color"/>
 </vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_low.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_low.xml
index 0528b68..a89ba0a 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_low.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_low.xml
@@ -15,23 +15,19 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
+        android:width="@dimen/hvac_panel_icon_dimen"
+        android:height="@dimen/hvac_panel_tall_icon_dimen"
+        android:viewportWidth="48"
+        android:viewportHeight="100">
   <path
       android:pathData="M11.522,0H9.708C8.311,0 7.033,0.705 6.408,1.821C4.369,5.463 3.834,9.63 4.898,13.59L11.268,37.284C11.404,37.79 12.136,37.932 12.495,37.52L16.362,33.081C19.399,29.596 19.855,24.842 17.528,20.946L12.459,12.461C11.081,10.155 11.271,7.354 12.949,5.213L13.276,4.797C14.324,3.461 14.146,1.646 12.854,0.492C12.5,0.177 12.022,0 11.522,0ZM38.065,36.811H25.871C23.839,36.811 21.858,37.378 20.205,38.432L14.336,42.174C13.636,42.62 13.552,43.521 14.161,44.065L15.714,45.451C17.542,47.083 20.021,48 22.607,48H38.065C41.142,48 43.636,45.773 43.636,43.027V41.784C43.636,39.037 41.142,36.811 38.065,36.811ZM39.06,13.841C38.405,14.32 38.089,14.748 37.933,15.092C37.782,15.428 37.739,15.787 37.799,16.216C37.933,17.181 38.537,18.271 39.372,19.674L39.506,19.899C40.228,21.107 41.121,22.603 41.377,24.089C41.525,24.942 41.481,25.861 41.069,26.767C40.66,27.663 39.952,28.411 39.001,29.04C38.31,29.497 37.38,29.307 36.923,28.616C36.466,27.925 36.656,26.994 37.347,26.537C37.96,26.132 38.221,25.78 38.339,25.523C38.451,25.276 38.488,24.99 38.421,24.599C38.265,23.692 37.648,22.643 36.794,21.209L36.775,21.176C36.019,19.906 35.059,18.293 34.827,16.63C34.703,15.738 34.778,14.795 35.197,13.862C35.613,12.938 36.319,12.129 37.289,11.42C37.957,10.931 38.896,11.076 39.385,11.745C39.874,12.414 39.729,13.352 39.06,13.841ZM31.797,15.092C31.952,14.748 32.269,14.32 32.924,13.841C33.592,13.352 33.738,12.414 33.248,11.745C32.759,11.076 31.821,10.931 31.152,11.42C30.183,12.129 29.476,12.938 29.061,13.862C28.641,14.795 28.566,15.738 28.691,16.63C28.923,18.293 29.883,19.906 30.639,21.176L30.658,21.209C31.511,22.643 32.128,23.692 32.285,24.599C32.352,24.99 32.315,25.276 32.202,25.523C32.085,25.78 31.823,26.132 31.211,26.537C30.52,26.994 30.33,27.925 30.787,28.616C31.243,29.307 32.174,29.497 32.865,29.04C33.816,28.411 34.524,27.663 34.932,26.767C35.345,25.861 35.388,24.942 35.241,24.089C34.985,22.603 34.092,21.107 33.37,19.899L33.236,19.674C32.401,18.271 31.797,17.181 31.662,16.216C31.602,15.787 31.646,15.428 31.797,15.092ZM26.787,13.841C26.132,14.32 25.816,14.748 25.661,15.092C25.51,15.428 25.466,15.787 25.526,16.216C25.66,17.181 26.264,18.271 27.1,19.674L27.234,19.899C27.955,21.107 28.848,22.603 29.105,24.089C29.252,24.942 29.208,25.861 28.796,26.767C28.388,27.663 27.68,28.411 26.729,29.04C26.038,29.497 25.107,29.307 24.65,28.616C24.193,27.925 24.383,26.994 25.074,26.537C25.687,26.132 25.949,25.78 26.066,25.523C26.178,25.276 26.216,24.99 26.148,24.599C25.992,23.692 25.375,22.643 24.522,21.209L24.502,21.176C23.747,19.906 22.786,18.293 22.555,16.63C22.43,15.738 22.505,14.795 22.925,13.862C23.34,12.938 24.046,12.129 25.016,11.42C25.684,10.931 26.623,11.076 27.112,11.745C27.601,12.414 27.456,13.352 26.787,13.841Z"
       android:fillColor="@color/hvac_on_icon_fill_color"
       android:fillType="evenOdd"/>
   <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
+      android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
       android:fillColor="@color/hvac_on_icon_fill_color"/>
   <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
+      android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
       android:fillColor="@color/hvac_on_icon_fill_color"
       android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
 </vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_med.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_med.xml
deleted file mode 100644
index 326ec7f..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_med.xml
+++ /dev/null
@@ -1,36 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M11.522,0H9.708C8.311,0 7.033,0.705 6.408,1.821C4.369,5.463 3.834,9.63 4.898,13.59L11.268,37.284C11.404,37.79 12.136,37.932 12.495,37.52L16.362,33.081C19.399,29.596 19.855,24.842 17.528,20.946L12.459,12.461C11.081,10.155 11.271,7.354 12.949,5.213L13.276,4.797C14.324,3.461 14.146,1.646 12.854,0.492C12.5,0.177 12.022,0 11.522,0ZM38.065,36.811H25.871C23.839,36.811 21.858,37.378 20.205,38.432L14.336,42.174C13.636,42.62 13.552,43.521 14.161,44.065L15.714,45.451C17.542,47.083 20.021,48 22.607,48H38.065C41.142,48 43.636,45.773 43.636,43.027V41.784C43.636,39.037 41.142,36.811 38.065,36.811ZM39.06,13.841C38.405,14.32 38.089,14.748 37.933,15.092C37.782,15.428 37.739,15.787 37.799,16.216C37.933,17.181 38.537,18.271 39.372,19.674L39.506,19.899C40.228,21.107 41.121,22.603 41.377,24.089C41.525,24.942 41.481,25.861 41.069,26.767C40.66,27.663 39.952,28.411 39.001,29.04C38.31,29.497 37.38,29.307 36.923,28.616C36.466,27.925 36.656,26.994 37.347,26.537C37.96,26.132 38.221,25.78 38.339,25.523C38.451,25.276 38.488,24.99 38.421,24.599C38.265,23.692 37.648,22.643 36.794,21.209L36.775,21.176C36.019,19.906 35.059,18.293 34.827,16.63C34.703,15.738 34.778,14.795 35.197,13.862C35.613,12.938 36.319,12.129 37.289,11.42C37.957,10.931 38.896,11.076 39.385,11.745C39.874,12.414 39.729,13.352 39.06,13.841ZM31.797,15.092C31.952,14.748 32.269,14.32 32.924,13.841C33.592,13.352 33.738,12.414 33.248,11.745C32.759,11.076 31.821,10.931 31.152,11.42C30.183,12.129 29.476,12.938 29.061,13.862C28.641,14.795 28.566,15.738 28.691,16.63C28.923,18.293 29.883,19.906 30.639,21.176L30.658,21.209C31.511,22.643 32.128,23.692 32.285,24.599C32.352,24.99 32.315,25.276 32.202,25.523C32.085,25.78 31.823,26.132 31.211,26.537C30.52,26.994 30.33,27.925 30.787,28.616C31.243,29.307 32.174,29.497 32.865,29.04C33.816,28.411 34.524,27.663 34.932,26.767C35.345,25.861 35.388,24.942 35.241,24.089C34.985,22.603 34.092,21.107 33.37,19.899L33.236,19.674C32.401,18.271 31.797,17.181 31.662,16.216C31.602,15.787 31.646,15.428 31.797,15.092ZM26.787,13.841C26.132,14.32 25.816,14.748 25.661,15.092C25.51,15.428 25.466,15.787 25.526,16.216C25.66,17.181 26.264,18.271 27.1,19.674L27.234,19.899C27.955,21.107 28.848,22.603 29.105,24.089C29.252,24.942 29.208,25.861 28.796,26.767C28.388,27.663 27.68,28.411 26.729,29.04C26.038,29.497 25.107,29.307 24.65,28.616C24.193,27.925 24.383,26.994 25.074,26.537C25.687,26.132 25.949,25.78 26.066,25.523C26.178,25.276 26.216,24.99 26.148,24.599C25.992,23.692 25.375,22.643 24.522,21.209L24.502,21.176C23.747,19.906 22.786,18.293 22.555,16.63C22.43,15.738 22.505,14.795 22.925,13.862C23.34,12.938 24.046,12.129 25.016,11.42C25.684,10.931 26.623,11.076 27.112,11.745C27.601,12.414 27.456,13.352 26.787,13.841Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillType="evenOdd"/>
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml
index 78d0236..9117cac 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml
@@ -15,24 +15,20 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
+        android:width="@dimen/hvac_panel_icon_dimen"
+        android:height="@dimen/hvac_panel_tall_icon_dimen"
+        android:viewportWidth="48"
+        android:viewportHeight="100">
   <path
       android:pathData="M11.522,0H9.708C8.311,0 7.033,0.705 6.408,1.821C4.369,5.463 3.834,9.63 4.898,13.59L11.268,37.284C11.404,37.79 12.136,37.932 12.495,37.52L16.362,33.081C19.399,29.596 19.855,24.842 17.528,20.946L12.459,12.461C11.081,10.155 11.271,7.354 12.949,5.213L13.276,4.797C14.324,3.461 14.146,1.646 12.854,0.492C12.5,0.177 12.022,0 11.522,0ZM38.065,36.811H25.871C23.839,36.811 21.858,37.378 20.205,38.432L14.336,42.174C13.636,42.62 13.552,43.521 14.161,44.065L15.714,45.451C17.542,47.083 20.021,48 22.607,48H38.065C41.142,48 43.636,45.773 43.636,43.027V41.784C43.636,39.037 41.142,36.811 38.065,36.811ZM39.06,13.841C38.405,14.32 38.089,14.748 37.933,15.092C37.782,15.428 37.739,15.787 37.799,16.216C37.933,17.181 38.537,18.271 39.372,19.674L39.506,19.899C40.228,21.107 41.121,22.603 41.377,24.089C41.525,24.942 41.481,25.861 41.069,26.767C40.66,27.663 39.952,28.411 39.001,29.04C38.31,29.497 37.38,29.307 36.923,28.616C36.466,27.925 36.656,26.994 37.347,26.537C37.96,26.132 38.221,25.78 38.339,25.523C38.451,25.276 38.488,24.99 38.421,24.599C38.265,23.692 37.648,22.643 36.794,21.209L36.775,21.176C36.019,19.906 35.059,18.293 34.827,16.63C34.703,15.738 34.778,14.795 35.197,13.862C35.613,12.938 36.319,12.129 37.289,11.42C37.957,10.931 38.896,11.076 39.385,11.745C39.874,12.414 39.729,13.352 39.06,13.841ZM31.797,15.092C31.952,14.748 32.269,14.32 32.924,13.841C33.592,13.352 33.738,12.414 33.248,11.745C32.759,11.076 31.821,10.931 31.152,11.42C30.183,12.129 29.476,12.938 29.061,13.862C28.641,14.795 28.566,15.738 28.691,16.63C28.923,18.293 29.883,19.906 30.639,21.176L30.658,21.209C31.511,22.643 32.128,23.692 32.285,24.599C32.352,24.99 32.315,25.276 32.202,25.523C32.085,25.78 31.823,26.132 31.211,26.537C30.52,26.994 30.33,27.925 30.787,28.616C31.243,29.307 32.174,29.497 32.865,29.04C33.816,28.411 34.524,27.663 34.932,26.767C35.345,25.861 35.388,24.942 35.241,24.089C34.985,22.603 34.092,21.107 33.37,19.899L33.236,19.674C32.401,18.271 31.797,17.181 31.662,16.216C31.602,15.787 31.646,15.428 31.797,15.092ZM26.787,13.841C26.132,14.32 25.816,14.748 25.661,15.092C25.51,15.428 25.466,15.787 25.526,16.216C25.66,17.181 26.264,18.271 27.1,19.674L27.234,19.899C27.955,21.107 28.848,22.603 29.105,24.089C29.252,24.942 29.208,25.861 28.796,26.767C28.388,27.663 27.68,28.411 26.729,29.04C26.038,29.497 25.107,29.307 24.65,28.616C24.193,27.925 24.383,26.994 25.074,26.537C25.687,26.132 25.949,25.78 26.066,25.523C26.178,25.276 26.216,24.99 26.148,24.599C25.992,23.692 25.375,22.643 24.522,21.209L24.502,21.176C23.747,19.906 22.786,18.293 22.555,16.63C22.43,15.738 22.505,14.795 22.925,13.862C23.34,12.938 24.046,12.129 25.016,11.42C25.684,10.931 26.623,11.076 27.112,11.745C27.601,12.414 27.456,13.352 26.787,13.841Z"
       android:fillColor="@color/hvac_off_icon_fill_color"
       android:fillType="evenOdd"/>
   <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
+      android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
       android:fillColor="@color/hvac_off_icon_fill_color"
       android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
   <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_off_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
+      android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
       android:fillColor="@color/hvac_off_icon_fill_color"
       android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
 </vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_off.xml
deleted file mode 100644
index 9d55f5c..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_off.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="48">
-  <path
-      android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
-      android:fillColor="@color/hvac_off_icon_fill_color"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_high.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_high.xml
deleted file mode 100644
index 8d92c3f..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_high.xml
+++ /dev/null
@@ -1,40 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M38.293,0H36.479C35.979,0 35.5,0.177 35.147,0.492C33.855,1.646 33.677,3.461 34.725,4.797L35.051,5.213C36.73,7.354 36.92,10.155 35.542,12.461L30.473,20.946C28.146,24.842 28.602,29.596 31.639,33.081L35.506,37.52C35.865,37.932 36.597,37.79 36.733,37.284L43.103,13.59C44.167,9.63 43.632,5.463 41.593,1.821C40.968,0.705 39.69,0 38.293,0Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M22.13,36.811H9.935C6.859,36.811 4.365,39.037 4.365,41.784V43.027C4.365,45.773 6.859,48 9.935,48H25.394C27.98,48 30.459,47.083 32.287,45.451L33.84,44.065C34.449,43.521 34.365,42.62 33.665,42.174L27.796,38.432C26.143,37.378 24.162,36.811 22.13,36.811Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M17.701,13.057L15.221,13.945C15.026,13.804 14.809,13.674 14.592,13.577C14.776,12.677 15.275,11.865 16.066,11.345C18.254,9.893 17.528,6.167 14.506,6.167C9.75,6.167 7.756,9.427 9.057,12.298L9.945,14.779C9.804,14.974 9.674,15.191 9.576,15.408C8.677,15.223 7.865,14.725 7.345,13.934C5.893,11.746 2.167,12.472 2.167,15.494C2.167,20.261 5.427,22.254 8.298,20.943L10.779,20.055C10.974,20.196 11.191,20.326 11.407,20.423C11.223,21.323 10.725,22.135 9.934,22.655C7.746,24.107 8.472,27.833 11.494,27.833C16.261,27.833 18.254,24.573 16.943,21.702L16.055,19.221C16.196,19.026 16.326,18.809 16.423,18.593C17.322,18.777 18.135,19.275 18.655,20.066C20.107,22.243 23.822,21.517 23.822,18.495C23.833,13.75 20.572,11.757 17.701,13.057ZM13,18.625C12.101,18.625 11.375,17.899 11.375,17C11.375,16.101 12.101,15.375 13,15.375C13.899,15.375 14.625,16.101 14.625,17C14.625,17.899 13.899,18.625 13,18.625Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_low.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_low.xml
deleted file mode 100644
index eb0927a..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_low.xml
+++ /dev/null
@@ -1,42 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M38.293,0H36.479C35.979,0 35.5,0.177 35.147,0.492C33.855,1.646 33.677,3.461 34.725,4.797L35.051,5.213C36.73,7.354 36.92,10.155 35.542,12.461L30.473,20.946C28.146,24.842 28.602,29.596 31.639,33.081L35.506,37.52C35.865,37.932 36.597,37.79 36.733,37.284L43.103,13.59C44.167,9.63 43.632,5.463 41.593,1.821C40.968,0.705 39.69,0 38.293,0Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M22.13,36.811H9.935C6.859,36.811 4.365,39.037 4.365,41.784V43.027C4.365,45.773 6.859,48 9.935,48H25.394C27.98,48 30.459,47.083 32.287,45.451L33.84,44.065C34.449,43.521 34.365,42.62 33.665,42.174L27.796,38.432C26.143,37.378 24.162,36.811 22.13,36.811Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M17.701,13.057L15.221,13.945C15.026,13.804 14.809,13.674 14.592,13.577C14.776,12.677 15.275,11.865 16.066,11.345C18.254,9.893 17.528,6.167 14.506,6.167C9.75,6.167 7.756,9.427 9.057,12.298L9.945,14.779C9.804,14.974 9.674,15.191 9.576,15.408C8.677,15.223 7.865,14.725 7.345,13.934C5.893,11.746 2.167,12.472 2.167,15.494C2.167,20.261 5.427,22.254 8.298,20.943L10.779,20.055C10.974,20.196 11.191,20.326 11.407,20.423C11.223,21.323 10.725,22.135 9.934,22.655C7.746,24.107 8.472,27.833 11.494,27.833C16.261,27.833 18.254,24.573 16.943,21.702L16.055,19.221C16.196,19.026 16.326,18.809 16.423,18.593C17.322,18.777 18.135,19.275 18.655,20.066C20.107,22.243 23.822,21.517 23.822,18.495C23.833,13.75 20.572,11.757 17.701,13.057ZM13,18.625C12.101,18.625 11.375,17.899 11.375,17C11.375,16.101 12.101,15.375 13,15.375C13.899,15.375 14.625,16.101 14.625,17C14.625,17.899 13.899,18.625 13,18.625Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_med.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_med.xml
deleted file mode 100644
index c7471a8..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_med.xml
+++ /dev/null
@@ -1,41 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M38.293,0H36.479C35.979,0 35.5,0.177 35.147,0.492C33.855,1.646 33.677,3.461 34.725,4.797L35.051,5.213C36.73,7.354 36.92,10.155 35.542,12.461L30.473,20.946C28.146,24.842 28.602,29.596 31.639,33.081L35.506,37.52C35.865,37.932 36.597,37.79 36.733,37.284L43.103,13.59C44.167,9.63 43.632,5.463 41.593,1.821C40.968,0.705 39.69,0 38.293,0Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M22.13,36.811H9.935C6.859,36.811 4.365,39.037 4.365,41.784V43.027C4.365,45.773 6.859,48 9.935,48H25.394C27.98,48 30.459,47.083 32.287,45.451L33.84,44.065C34.449,43.521 34.365,42.62 33.665,42.174L27.796,38.432C26.143,37.378 24.162,36.811 22.13,36.811Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M17.701,13.057L15.221,13.945C15.026,13.804 14.809,13.674 14.592,13.577C14.776,12.677 15.275,11.865 16.066,11.345C18.254,9.893 17.528,6.167 14.506,6.167C9.75,6.167 7.756,9.427 9.057,12.298L9.945,14.779C9.804,14.974 9.674,15.191 9.576,15.408C8.677,15.223 7.865,14.725 7.345,13.934C5.893,11.746 2.167,12.472 2.167,15.494C2.167,20.261 5.427,22.254 8.298,20.943L10.779,20.055C10.974,20.196 11.191,20.326 11.407,20.423C11.223,21.323 10.725,22.135 9.934,22.655C7.746,24.107 8.472,27.833 11.494,27.833C16.261,27.833 18.254,24.573 16.943,21.702L16.055,19.221C16.196,19.026 16.326,18.809 16.423,18.593C17.322,18.777 18.135,19.275 18.655,20.066C20.107,22.243 23.822,21.517 23.822,18.495C23.833,13.75 20.572,11.757 17.701,13.057ZM13,18.625C12.101,18.625 11.375,17.899 11.375,17C11.375,16.101 12.101,15.375 13,15.375C13.899,15.375 14.625,16.101 14.625,17C14.625,17.899 13.899,18.625 13,18.625Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_off.xml
deleted file mode 100644
index f0df11c..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_cool_off.xml
+++ /dev/null
@@ -1,43 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M38.293,0H36.479C35.979,0 35.5,0.177 35.147,0.492C33.855,1.646 33.677,3.461 34.725,4.797L35.051,5.213C36.73,7.354 36.92,10.155 35.542,12.461L30.473,20.946C28.146,24.842 28.602,29.596 31.639,33.081L35.506,37.52C35.865,37.932 36.597,37.79 36.733,37.284L43.103,13.59C44.167,9.63 43.632,5.463 41.593,1.821C40.968,0.705 39.69,0 38.293,0Z"
-      android:fillColor="@color/hvac_off_icon_fill_color"/>
-  <path
-      android:pathData="M22.13,36.811H9.935C6.859,36.811 4.365,39.037 4.365,41.784V43.027C4.365,45.773 6.859,48 9.935,48H25.394C27.98,48 30.459,47.083 32.287,45.451L33.84,44.065C34.449,43.521 34.365,42.62 33.665,42.174L27.796,38.432C26.143,37.378 24.162,36.811 22.13,36.811Z"
-      android:fillColor="@color/hvac_off_icon_fill_color"/>
-  <path
-      android:pathData="M17.701,13.057L15.221,13.945C15.026,13.804 14.809,13.674 14.592,13.577C14.776,12.677 15.275,11.865 16.066,11.345C18.254,9.893 17.528,6.167 14.506,6.167C9.75,6.167 7.756,9.427 9.057,12.298L9.945,14.779C9.804,14.974 9.674,15.191 9.576,15.408C8.677,15.223 7.865,14.725 7.345,13.934C5.893,11.746 2.167,12.472 2.167,15.494C2.167,20.261 5.427,22.254 8.298,20.943L10.779,20.055C10.974,20.196 11.191,20.326 11.407,20.423C11.223,21.323 10.725,22.135 9.934,22.655C7.746,24.107 8.472,27.833 11.494,27.833C16.261,27.833 18.254,24.573 16.943,21.702L16.055,19.221C16.196,19.026 16.326,18.809 16.423,18.593C17.322,18.777 18.135,19.275 18.655,20.066C20.107,22.243 23.822,21.517 23.822,18.495C23.833,13.75 20.572,11.757 17.701,13.057ZM13,18.625C12.101,18.625 11.375,17.899 11.375,17C11.375,16.101 12.101,15.375 13,15.375C13.899,15.375 14.625,16.101 14.625,17C14.625,17.899 13.899,18.625 13,18.625Z"
-      android:fillColor="@color/hvac_off_icon_fill_color"/>
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_off_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_off_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_off_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml
index 8e5c5e3..0d02e85 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml
@@ -15,21 +15,18 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
+        android:width="@dimen/hvac_panel_icon_dimen"
+        android:height="@dimen/hvac_panel_tall_icon_dimen"
+        android:viewportWidth="48"
+        android:viewportHeight="100">
   <path
-      android:pathData="M36.479,0H38.293C39.69,0 40.968,0.705 41.593,1.821C43.632,5.463 44.167,9.63 43.103,13.59L36.733,37.284C36.597,37.79 35.865,37.932 35.506,37.52L31.639,33.081C28.602,29.596 28.146,24.842 30.473,20.946L35.542,12.461C36.92,10.155 36.73,7.354 35.051,5.213L34.725,4.797C33.677,3.461 33.855,1.646 35.147,0.492C35.5,0.177 35.979,0 36.479,0ZM9.935,36.811H22.13C24.162,36.811 26.143,37.378 27.796,38.432L33.665,42.174C34.365,42.62 34.449,43.521 33.84,44.065L32.287,45.451C30.459,47.083 27.98,48 25.394,48H9.935C6.859,48 4.365,45.773 4.365,43.027V41.784C4.365,39.037 6.859,36.811 9.935,36.811ZM23.103,13.84C22.448,14.319 22.132,14.746 21.977,15.091C21.826,15.427 21.782,15.786 21.842,16.215C21.976,17.179 22.58,18.27 23.416,19.673L23.55,19.898C24.271,21.106 25.164,22.602 25.421,24.088C25.568,24.941 25.524,25.86 25.112,26.766C24.704,27.662 23.996,28.41 23.045,29.039C22.354,29.496 21.423,29.306 20.966,28.615C20.509,27.924 20.699,26.993 21.39,26.536C22.003,26.131 22.265,25.779 22.382,25.522C22.494,25.275 22.532,24.989 22.464,24.598C22.308,23.691 21.691,22.641 20.838,21.207L20.818,21.175C20.063,19.905 19.102,18.292 18.871,16.629C18.746,15.737 18.821,14.794 19.241,13.861C19.656,12.937 20.362,12.128 21.332,11.419C22,10.93 22.939,11.075 23.428,11.744C23.917,12.412 23.772,13.351 23.103,13.84ZM15.843,15.091C15.998,14.746 16.315,14.319 16.969,13.84C17.638,13.351 17.783,12.412 17.294,11.744C16.805,11.075 15.866,10.93 15.198,11.419C14.228,12.128 13.522,12.937 13.107,13.861C12.687,14.794 12.612,15.737 12.736,16.629C12.968,18.292 13.929,19.905 14.684,21.175L14.704,21.207C15.557,22.641 16.174,23.691 16.33,24.598C16.398,24.989 16.36,25.275 16.248,25.522C16.131,25.779 15.869,26.131 15.256,26.536C14.565,26.993 14.375,27.924 14.832,28.615C15.289,29.306 16.22,29.496 16.911,29.039C17.862,28.41 18.57,27.662 18.978,26.766C19.39,25.86 19.434,24.941 19.287,24.088C19.031,22.602 18.137,21.106 17.416,19.898L17.282,19.673C16.446,18.27 15.842,17.179 15.708,16.215C15.648,15.786 15.692,15.427 15.843,15.091ZM10.831,13.84C10.177,14.319 9.86,14.746 9.705,15.091C9.554,15.427 9.51,15.786 9.57,16.215C9.705,17.179 10.309,18.27 11.144,19.673L11.278,19.898C12,21.106 12.893,22.602 13.149,24.088C13.296,24.941 13.253,25.86 12.84,26.766C12.432,27.662 11.724,28.41 10.773,29.039C10.082,29.496 9.151,29.306 8.694,28.615C8.238,27.924 8.427,26.993 9.119,26.536C9.731,26.131 9.993,25.779 10.11,25.522C10.223,25.275 10.26,24.989 10.193,24.598C10.036,23.691 9.419,22.641 8.566,21.207L8.547,21.175C7.791,19.905 6.831,18.292 6.599,16.629C6.474,15.737 6.549,14.794 6.969,13.861C7.384,12.937 8.09,12.128 9.06,11.419C9.729,10.93 10.667,11.075 11.156,11.744C11.646,12.412 11.5,13.351 10.831,13.84Z"
+      android:pathData="M36.48,0H38.294C39.691,0 40.969,0.705 41.594,1.821C43.633,5.463 44.168,9.63 43.104,13.59L36.734,37.284C36.598,37.79 35.866,37.932 35.507,37.52L31.64,33.081C28.603,29.596 28.147,24.842 30.474,20.946L35.543,12.461C36.921,10.155 36.731,7.354 35.053,5.213L34.726,4.797C33.678,3.461 33.856,1.646 35.148,0.492C35.501,0.177 35.98,0 36.48,0ZM9.936,36.811H22.131C24.163,36.811 26.144,37.378 27.797,38.432L33.666,42.174C34.366,42.62 34.45,43.521 33.841,44.065L32.288,45.451C30.46,47.083 27.981,48 25.395,48H9.936C6.86,48 4.366,45.773 4.366,43.027V41.784C4.366,39.037 6.86,36.811 9.936,36.811ZM23.104,13.84C22.449,14.319 22.133,14.746 21.978,15.091C21.827,15.427 21.783,15.786 21.843,16.215C21.977,17.179 22.581,18.27 23.417,19.673L23.551,19.898C24.272,21.106 25.166,22.602 25.422,24.088C25.569,24.941 25.525,25.86 25.113,26.766C24.705,27.662 23.997,28.41 23.046,29.039C22.354,29.496 21.424,29.306 20.967,28.615C20.51,27.924 20.7,26.993 21.391,26.536C22.004,26.131 22.266,25.779 22.383,25.522C22.495,25.275 22.533,24.989 22.465,24.598C22.309,23.691 21.692,22.641 20.839,21.207L20.819,21.175C20.063,19.905 19.103,18.292 18.871,16.629C18.747,15.737 18.822,14.794 19.242,13.861C19.657,12.937 20.363,12.128 21.333,11.419C22.001,10.93 22.94,11.075 23.429,11.744C23.918,12.412 23.773,13.351 23.104,13.84ZM15.844,15.091C15.999,14.746 16.316,14.319 16.97,13.84C17.639,13.351 17.784,12.412 17.295,11.744C16.806,11.075 15.867,10.93 15.199,11.419C14.229,12.128 13.523,12.937 13.108,13.861C12.688,14.794 12.613,15.737 12.738,16.629C12.969,18.292 13.929,19.905 14.685,21.175L14.705,21.207C15.558,22.641 16.175,23.691 16.331,24.598C16.399,24.989 16.361,25.275 16.249,25.522C16.132,25.779 15.87,26.131 15.257,26.536C14.566,26.993 14.376,27.924 14.833,28.615C15.29,29.306 16.221,29.496 16.912,29.039C17.862,28.41 18.571,27.662 18.979,26.766C19.392,25.86 19.435,24.941 19.288,24.088C19.031,22.602 18.138,21.106 17.417,19.898L17.283,19.673C16.447,18.27 15.843,17.179 15.709,16.215C15.649,15.786 15.693,15.427 15.844,15.091ZM10.832,13.84C10.178,14.319 9.861,14.746 9.706,15.091C9.555,15.427 9.511,15.786 9.571,16.215C9.705,17.179 10.31,18.27 11.145,19.673L11.279,19.898C12,21.106 12.894,22.602 13.15,24.088C13.297,24.941 13.254,25.86 12.841,26.766C12.433,27.662 11.725,28.41 10.774,29.039C10.083,29.496 9.152,29.306 8.695,28.615C8.239,27.924 8.428,26.993 9.12,26.536C9.732,26.131 9.994,25.779 10.111,25.522C10.224,25.275 10.261,24.989 10.194,24.598C10.037,23.691 9.42,22.641 8.567,21.207L8.548,21.175C7.792,19.905 6.832,18.292 6.6,16.629C6.475,15.737 6.55,14.794 6.97,13.861C7.385,12.937 8.091,12.128 9.061,11.419C9.73,10.93 10.668,11.075 11.157,11.744C11.646,12.412 11.501,13.351 10.832,13.84Z"
       android:fillColor="@color/hvac_on_icon_fill_color"
       android:fillType="evenOdd"/>
   <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
+      android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
       android:fillColor="@color/hvac_on_icon_fill_color"/>
   <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
+      android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
       android:fillColor="@color/hvac_on_icon_fill_color"/>
 </vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_low.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_low.xml
index 52cf98b..b95b31a 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_low.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_low.xml
@@ -15,23 +15,19 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
+        android:width="@dimen/hvac_panel_icon_dimen"
+        android:height="@dimen/hvac_panel_tall_icon_dimen"
+        android:viewportWidth="48"
+        android:viewportHeight="100">
   <path
-      android:pathData="M36.479,0H38.293C39.69,0 40.968,0.705 41.593,1.821C43.632,5.463 44.167,9.63 43.103,13.59L36.733,37.284C36.597,37.79 35.865,37.932 35.506,37.52L31.639,33.081C28.602,29.596 28.146,24.842 30.473,20.946L35.542,12.461C36.92,10.155 36.73,7.354 35.051,5.213L34.725,4.797C33.677,3.461 33.855,1.646 35.147,0.492C35.5,0.177 35.979,0 36.479,0ZM9.935,36.811H22.13C24.162,36.811 26.143,37.378 27.796,38.432L33.665,42.174C34.365,42.62 34.449,43.521 33.84,44.065L32.287,45.451C30.459,47.083 27.98,48 25.394,48H9.935C6.859,48 4.365,45.773 4.365,43.027V41.784C4.365,39.037 6.859,36.811 9.935,36.811ZM23.103,13.84C22.448,14.319 22.132,14.746 21.977,15.091C21.826,15.427 21.782,15.786 21.842,16.215C21.976,17.179 22.58,18.27 23.416,19.673L23.55,19.898C24.271,21.106 25.164,22.602 25.421,24.088C25.568,24.941 25.524,25.86 25.112,26.766C24.704,27.662 23.996,28.41 23.045,29.039C22.354,29.496 21.423,29.306 20.966,28.615C20.509,27.924 20.699,26.993 21.39,26.536C22.003,26.131 22.265,25.779 22.382,25.522C22.494,25.275 22.532,24.989 22.464,24.598C22.308,23.691 21.691,22.641 20.838,21.207L20.818,21.175C20.063,19.905 19.102,18.292 18.871,16.629C18.746,15.737 18.821,14.794 19.241,13.861C19.656,12.937 20.362,12.128 21.332,11.419C22,10.93 22.939,11.075 23.428,11.744C23.917,12.412 23.772,13.351 23.103,13.84ZM15.843,15.091C15.998,14.746 16.315,14.319 16.969,13.84C17.638,13.351 17.783,12.412 17.294,11.744C16.805,11.075 15.866,10.93 15.198,11.419C14.228,12.128 13.522,12.937 13.107,13.861C12.687,14.794 12.612,15.737 12.736,16.629C12.968,18.292 13.929,19.905 14.684,21.175L14.704,21.207C15.557,22.641 16.174,23.691 16.33,24.598C16.398,24.989 16.36,25.275 16.248,25.522C16.131,25.779 15.869,26.131 15.256,26.536C14.565,26.993 14.375,27.924 14.832,28.615C15.289,29.306 16.22,29.496 16.911,29.039C17.862,28.41 18.57,27.662 18.978,26.766C19.39,25.86 19.434,24.941 19.287,24.088C19.031,22.602 18.137,21.106 17.416,19.898L17.282,19.673C16.446,18.27 15.842,17.179 15.708,16.215C15.648,15.786 15.692,15.427 15.843,15.091ZM10.831,13.84C10.177,14.319 9.86,14.746 9.705,15.091C9.554,15.427 9.51,15.786 9.57,16.215C9.705,17.179 10.309,18.27 11.144,19.673L11.278,19.898C12,21.106 12.893,22.602 13.149,24.088C13.296,24.941 13.253,25.86 12.84,26.766C12.432,27.662 11.724,28.41 10.773,29.039C10.082,29.496 9.151,29.306 8.694,28.615C8.238,27.924 8.427,26.993 9.119,26.536C9.731,26.131 9.993,25.779 10.11,25.522C10.223,25.275 10.26,24.989 10.193,24.598C10.036,23.691 9.419,22.641 8.566,21.207L8.547,21.175C7.791,19.905 6.831,18.292 6.599,16.629C6.474,15.737 6.549,14.794 6.969,13.861C7.384,12.937 8.09,12.128 9.06,11.419C9.729,10.93 10.667,11.075 11.156,11.744C11.646,12.412 11.5,13.351 10.831,13.84Z"
+      android:pathData="M36.48,0H38.294C39.691,0 40.969,0.705 41.594,1.821C43.633,5.463 44.168,9.63 43.104,13.59L36.734,37.284C36.598,37.79 35.866,37.932 35.507,37.52L31.64,33.081C28.603,29.596 28.147,24.842 30.474,20.946L35.543,12.461C36.921,10.155 36.731,7.354 35.053,5.213L34.726,4.797C33.678,3.461 33.856,1.646 35.148,0.492C35.501,0.177 35.98,0 36.48,0ZM9.936,36.811H22.131C24.163,36.811 26.144,37.378 27.797,38.432L33.666,42.174C34.366,42.62 34.45,43.521 33.841,44.065L32.288,45.451C30.46,47.083 27.981,48 25.395,48H9.936C6.86,48 4.366,45.773 4.366,43.027V41.784C4.366,39.037 6.86,36.811 9.936,36.811ZM23.104,13.84C22.449,14.319 22.133,14.746 21.978,15.091C21.827,15.427 21.783,15.786 21.843,16.215C21.977,17.179 22.581,18.27 23.417,19.673L23.551,19.898C24.272,21.106 25.166,22.602 25.422,24.088C25.569,24.941 25.525,25.86 25.113,26.766C24.705,27.662 23.997,28.41 23.046,29.039C22.354,29.496 21.424,29.306 20.967,28.615C20.51,27.924 20.7,26.993 21.391,26.536C22.004,26.131 22.266,25.779 22.383,25.522C22.495,25.275 22.533,24.989 22.465,24.598C22.309,23.691 21.692,22.641 20.839,21.207L20.819,21.175C20.063,19.905 19.103,18.292 18.871,16.629C18.747,15.737 18.822,14.794 19.242,13.861C19.657,12.937 20.363,12.128 21.333,11.419C22.001,10.93 22.94,11.075 23.429,11.744C23.918,12.412 23.773,13.351 23.104,13.84ZM15.844,15.091C15.999,14.746 16.316,14.319 16.97,13.84C17.639,13.351 17.784,12.412 17.295,11.744C16.806,11.075 15.867,10.93 15.199,11.419C14.229,12.128 13.523,12.937 13.108,13.861C12.688,14.794 12.613,15.737 12.738,16.629C12.969,18.292 13.929,19.905 14.685,21.175L14.705,21.207C15.558,22.641 16.175,23.691 16.331,24.598C16.399,24.989 16.361,25.275 16.249,25.522C16.132,25.779 15.87,26.131 15.257,26.536C14.566,26.993 14.376,27.924 14.833,28.615C15.29,29.306 16.221,29.496 16.912,29.039C17.862,28.41 18.571,27.662 18.979,26.766C19.392,25.86 19.435,24.941 19.288,24.088C19.031,22.602 18.138,21.106 17.417,19.898L17.283,19.673C16.447,18.27 15.843,17.179 15.709,16.215C15.649,15.786 15.693,15.427 15.844,15.091ZM10.832,13.84C10.178,14.319 9.861,14.746 9.706,15.091C9.555,15.427 9.511,15.786 9.571,16.215C9.705,17.179 10.31,18.27 11.145,19.673L11.279,19.898C12,21.106 12.894,22.602 13.15,24.088C13.297,24.941 13.254,25.86 12.841,26.766C12.433,27.662 11.725,28.41 10.774,29.039C10.083,29.496 9.152,29.306 8.695,28.615C8.239,27.924 8.428,26.993 9.12,26.536C9.732,26.131 9.994,25.779 10.111,25.522C10.224,25.275 10.261,24.989 10.194,24.598C10.037,23.691 9.42,22.641 8.567,21.207L8.548,21.175C7.792,19.905 6.832,18.292 6.6,16.629C6.475,15.737 6.55,14.794 6.97,13.861C7.385,12.937 8.091,12.128 9.061,11.419C9.73,10.93 10.668,11.075 11.157,11.744C11.646,12.412 11.501,13.351 10.832,13.84Z"
       android:fillColor="@color/hvac_on_icon_fill_color"
       android:fillType="evenOdd"/>
   <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
+      android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
       android:fillColor="@color/hvac_on_icon_fill_color"/>
   <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
+      android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
       android:fillColor="@color/hvac_on_icon_fill_color"
       android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
 </vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_med.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_med.xml
deleted file mode 100644
index 8a02789..0000000
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_med.xml
+++ /dev/null
@@ -1,36 +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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
-  <path
-      android:pathData="M36.479,0H38.293C39.69,0 40.968,0.705 41.593,1.821C43.632,5.463 44.167,9.63 43.103,13.59L36.733,37.284C36.597,37.79 35.865,37.932 35.506,37.52L31.639,33.081C28.602,29.596 28.146,24.842 30.473,20.946L35.542,12.461C36.92,10.155 36.73,7.354 35.051,5.213L34.725,4.797C33.677,3.461 33.855,1.646 35.147,0.492C35.5,0.177 35.979,0 36.479,0ZM9.935,36.811H22.13C24.162,36.811 26.143,37.378 27.796,38.432L33.665,42.174C34.365,42.62 34.449,43.521 33.84,44.065L32.287,45.451C30.459,47.083 27.98,48 25.394,48H9.935C6.859,48 4.365,45.773 4.365,43.027V41.784C4.365,39.037 6.859,36.811 9.935,36.811ZM23.103,13.84C22.448,14.319 22.132,14.746 21.977,15.091C21.826,15.427 21.782,15.786 21.842,16.215C21.976,17.179 22.58,18.27 23.416,19.673L23.55,19.898C24.271,21.106 25.164,22.602 25.421,24.088C25.568,24.941 25.524,25.86 25.112,26.766C24.704,27.662 23.996,28.41 23.045,29.039C22.354,29.496 21.423,29.306 20.966,28.615C20.509,27.924 20.699,26.993 21.39,26.536C22.003,26.131 22.265,25.779 22.382,25.522C22.494,25.275 22.532,24.989 22.464,24.598C22.308,23.691 21.691,22.641 20.838,21.207L20.818,21.175C20.063,19.905 19.102,18.292 18.871,16.629C18.746,15.737 18.821,14.794 19.241,13.861C19.656,12.937 20.362,12.128 21.332,11.419C22,10.93 22.939,11.075 23.428,11.744C23.917,12.412 23.772,13.351 23.103,13.84ZM15.843,15.091C15.998,14.746 16.315,14.319 16.969,13.84C17.638,13.351 17.783,12.412 17.294,11.744C16.805,11.075 15.866,10.93 15.198,11.419C14.228,12.128 13.522,12.937 13.107,13.861C12.687,14.794 12.612,15.737 12.736,16.629C12.968,18.292 13.929,19.905 14.684,21.175L14.704,21.207C15.557,22.641 16.174,23.691 16.33,24.598C16.398,24.989 16.36,25.275 16.248,25.522C16.131,25.779 15.869,26.131 15.256,26.536C14.565,26.993 14.375,27.924 14.832,28.615C15.289,29.306 16.22,29.496 16.911,29.039C17.862,28.41 18.57,27.662 18.978,26.766C19.39,25.86 19.434,24.941 19.287,24.088C19.031,22.602 18.137,21.106 17.416,19.898L17.282,19.673C16.446,18.27 15.842,17.179 15.708,16.215C15.648,15.786 15.692,15.427 15.843,15.091ZM10.831,13.84C10.177,14.319 9.86,14.746 9.705,15.091C9.554,15.427 9.51,15.786 9.57,16.215C9.705,17.179 10.309,18.27 11.144,19.673L11.278,19.898C12,21.106 12.893,22.602 13.149,24.088C13.296,24.941 13.253,25.86 12.84,26.766C12.432,27.662 11.724,28.41 10.773,29.039C10.082,29.496 9.151,29.306 8.694,28.615C8.238,27.924 8.427,26.993 9.119,26.536C9.731,26.131 9.993,25.779 10.11,25.522C10.223,25.275 10.26,24.989 10.193,24.598C10.036,23.691 9.419,22.641 8.566,21.207L8.547,21.175C7.791,19.905 6.831,18.292 6.599,16.629C6.474,15.737 6.549,14.794 6.969,13.861C7.384,12.937 8.09,12.128 9.06,11.419C9.729,10.93 10.667,11.075 11.156,11.744C11.646,12.412 11.5,13.351 10.831,13.84Z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillType="evenOdd"/>
-  <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_on_icon_fill_color"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
-      android:fillColor="@color/hvac_on_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml
index b407df6..cbf3fd1 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml
@@ -15,24 +15,20 @@
   -->
 
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/hvac_panel_icon_dimen"
-    android:height="@dimen/hvac_panel_tall_icon_dimen"
-    android:viewportWidth="48"
-    android:viewportHeight="108">
+        android:width="@dimen/hvac_panel_icon_dimen"
+        android:height="@dimen/hvac_panel_tall_icon_dimen"
+        android:viewportWidth="48"
+        android:viewportHeight="100">
   <path
-      android:pathData="M36.479,0H38.293C39.69,0 40.968,0.705 41.593,1.821C43.632,5.463 44.167,9.63 43.103,13.59L36.733,37.284C36.597,37.79 35.865,37.932 35.506,37.52L31.639,33.081C28.602,29.596 28.146,24.842 30.473,20.946L35.542,12.461C36.92,10.155 36.73,7.354 35.051,5.213L34.725,4.797C33.677,3.461 33.855,1.646 35.147,0.492C35.5,0.177 35.979,0 36.479,0ZM9.935,36.811H22.13C24.162,36.811 26.143,37.378 27.796,38.432L33.665,42.174C34.365,42.62 34.449,43.521 33.84,44.065L32.287,45.451C30.459,47.083 27.98,48 25.394,48H9.935C6.859,48 4.365,45.773 4.365,43.027V41.784C4.365,39.037 6.859,36.811 9.935,36.811ZM23.103,13.84C22.448,14.319 22.132,14.746 21.977,15.091C21.826,15.427 21.782,15.786 21.842,16.215C21.976,17.179 22.58,18.27 23.416,19.673L23.55,19.898C24.271,21.106 25.164,22.602 25.421,24.088C25.568,24.941 25.524,25.86 25.112,26.766C24.704,27.662 23.996,28.41 23.045,29.039C22.354,29.496 21.423,29.306 20.966,28.615C20.509,27.924 20.699,26.993 21.39,26.536C22.003,26.131 22.265,25.779 22.382,25.522C22.494,25.275 22.532,24.989 22.464,24.598C22.308,23.691 21.691,22.641 20.838,21.207L20.818,21.175C20.063,19.905 19.102,18.292 18.871,16.629C18.746,15.737 18.821,14.794 19.241,13.861C19.656,12.937 20.362,12.128 21.332,11.419C22,10.93 22.939,11.075 23.428,11.744C23.917,12.412 23.772,13.351 23.103,13.84ZM15.843,15.091C15.998,14.746 16.315,14.319 16.969,13.84C17.638,13.351 17.783,12.412 17.294,11.744C16.805,11.075 15.866,10.93 15.198,11.419C14.228,12.128 13.522,12.937 13.107,13.861C12.687,14.794 12.612,15.737 12.736,16.629C12.968,18.292 13.929,19.905 14.684,21.175L14.704,21.207C15.557,22.641 16.174,23.691 16.33,24.598C16.398,24.989 16.36,25.275 16.248,25.522C16.131,25.779 15.869,26.131 15.256,26.536C14.565,26.993 14.375,27.924 14.832,28.615C15.289,29.306 16.22,29.496 16.911,29.039C17.862,28.41 18.57,27.662 18.978,26.766C19.39,25.86 19.434,24.941 19.287,24.088C19.031,22.602 18.137,21.106 17.416,19.898L17.282,19.673C16.446,18.27 15.842,17.179 15.708,16.215C15.648,15.786 15.692,15.427 15.843,15.091ZM10.831,13.84C10.177,14.319 9.86,14.746 9.705,15.091C9.554,15.427 9.51,15.786 9.57,16.215C9.705,17.179 10.309,18.27 11.144,19.673L11.278,19.898C12,21.106 12.893,22.602 13.149,24.088C13.296,24.941 13.253,25.86 12.84,26.766C12.432,27.662 11.724,28.41 10.773,29.039C10.082,29.496 9.151,29.306 8.694,28.615C8.238,27.924 8.427,26.993 9.119,26.536C9.731,26.131 9.993,25.779 10.11,25.522C10.223,25.275 10.26,24.989 10.193,24.598C10.036,23.691 9.419,22.641 8.566,21.207L8.547,21.175C7.791,19.905 6.831,18.292 6.599,16.629C6.474,15.737 6.549,14.794 6.969,13.861C7.384,12.937 8.09,12.128 9.06,11.419C9.729,10.93 10.667,11.075 11.156,11.744C11.646,12.412 11.5,13.351 10.831,13.84Z"
+      android:pathData="M36.48,0H38.294C39.691,0 40.969,0.705 41.594,1.821C43.633,5.463 44.168,9.63 43.104,13.59L36.734,37.284C36.598,37.79 35.866,37.932 35.507,37.52L31.64,33.081C28.603,29.596 28.147,24.842 30.474,20.946L35.543,12.461C36.921,10.155 36.731,7.354 35.053,5.213L34.726,4.797C33.678,3.461 33.856,1.646 35.148,0.492C35.501,0.177 35.98,0 36.48,0ZM9.936,36.811H22.131C24.163,36.811 26.144,37.378 27.797,38.432L33.666,42.174C34.366,42.62 34.45,43.521 33.841,44.065L32.288,45.451C30.46,47.083 27.981,48 25.395,48H9.936C6.86,48 4.366,45.773 4.366,43.027V41.784C4.366,39.037 6.86,36.811 9.936,36.811ZM23.104,13.84C22.449,14.319 22.133,14.746 21.978,15.091C21.827,15.427 21.783,15.786 21.843,16.215C21.977,17.179 22.581,18.27 23.417,19.673L23.551,19.898C24.272,21.106 25.166,22.602 25.422,24.088C25.569,24.941 25.525,25.86 25.113,26.766C24.705,27.662 23.997,28.41 23.046,29.039C22.354,29.496 21.424,29.306 20.967,28.615C20.51,27.924 20.7,26.993 21.391,26.536C22.004,26.131 22.266,25.779 22.383,25.522C22.495,25.275 22.533,24.989 22.465,24.598C22.309,23.691 21.692,22.641 20.839,21.207L20.819,21.175C20.063,19.905 19.103,18.292 18.871,16.629C18.747,15.737 18.822,14.794 19.242,13.861C19.657,12.937 20.363,12.128 21.333,11.419C22.001,10.93 22.94,11.075 23.429,11.744C23.918,12.412 23.773,13.351 23.104,13.84ZM15.844,15.091C15.999,14.746 16.316,14.319 16.97,13.84C17.639,13.351 17.784,12.412 17.295,11.744C16.806,11.075 15.867,10.93 15.199,11.419C14.229,12.128 13.523,12.937 13.108,13.861C12.688,14.794 12.613,15.737 12.738,16.629C12.969,18.292 13.929,19.905 14.685,21.175L14.705,21.207C15.558,22.641 16.175,23.691 16.331,24.598C16.399,24.989 16.361,25.275 16.249,25.522C16.132,25.779 15.87,26.131 15.257,26.536C14.566,26.993 14.376,27.924 14.833,28.615C15.29,29.306 16.221,29.496 16.912,29.039C17.862,28.41 18.571,27.662 18.979,26.766C19.392,25.86 19.435,24.941 19.288,24.088C19.031,22.602 18.138,21.106 17.417,19.898L17.283,19.673C16.447,18.27 15.843,17.179 15.709,16.215C15.649,15.786 15.693,15.427 15.844,15.091ZM10.832,13.84C10.178,14.319 9.861,14.746 9.706,15.091C9.555,15.427 9.511,15.786 9.571,16.215C9.705,17.179 10.31,18.27 11.145,19.673L11.279,19.898C12,21.106 12.894,22.602 13.15,24.088C13.297,24.941 13.254,25.86 12.841,26.766C12.433,27.662 11.725,28.41 10.774,29.039C10.083,29.496 9.152,29.306 8.695,28.615C8.239,27.924 8.428,26.993 9.12,26.536C9.732,26.131 9.994,25.779 10.111,25.522C10.224,25.275 10.261,24.989 10.194,24.598C10.037,23.691 9.42,22.641 8.567,21.207L8.548,21.175C7.792,19.905 6.832,18.292 6.6,16.629C6.475,15.737 6.55,14.794 6.97,13.861C7.385,12.937 8.091,12.128 9.061,11.419C9.73,10.93 10.668,11.075 11.157,11.744C11.646,12.412 11.501,13.351 10.832,13.84Z"
       android:fillColor="@color/hvac_off_icon_fill_color"
       android:fillType="evenOdd"/>
   <path
-      android:pathData="M24,79L24,79A5,5 0,0 1,29 84L29,84A5,5 0,0 1,24 89L24,89A5,5 0,0 1,19 84L19,84A5,5 0,0 1,24 79z"
+      android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
       android:fillColor="@color/hvac_off_icon_fill_color"
       android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
   <path
-      android:pathData="M24,97L24,97A5,5 0,0 1,29 102L29,102A5,5 0,0 1,24 107L24,107A5,5 0,0 1,19 102L19,102A5,5 0,0 1,24 97z"
-      android:fillColor="@color/hvac_off_icon_fill_color"
-      android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
-  <path
-      android:pathData="M24,61L24,61A5,5 0,0 1,29 66L29,66A5,5 0,0 1,24 71L24,71A5,5 0,0 1,19 66L19,66A5,5 0,0 1,24 61z"
+      android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
       android:fillColor="@color/hvac_off_icon_fill_color"
       android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
 </vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_bottom_system_bar.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_bottom_system_bar.xml
index 974cec2..557547a 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_bottom_system_bar.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_bottom_system_bar.xml
@@ -93,4 +93,35 @@
         </com.android.systemui.car.hvac.TemperatureControlView>
 
     </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/occlusion_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:gravity="center"
+        android:layoutDirection="ltr"
+        android:visibility="gone">
+        <com.android.systemui.car.hvac.TemperatureControlView
+            android:id="@+id/driver_hvac"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:layout_gravity="start"
+            android:gravity="start|center_vertical"
+            systemui:hvacAreaId="49">
+            <include layout="@layout/adjustable_temperature_view"/>
+        </com.android.systemui.car.hvac.TemperatureControlView>
+
+        <com.android.systemui.car.hvac.TemperatureControlView
+            android:id="@+id/passenger_hvac"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_gravity="end"
+            android:layout_weight="1"
+            android:gravity="end|center_vertical"
+            systemui:hvacAreaId="68">
+            <include layout="@layout/adjustable_temperature_view"/>
+        </com.android.systemui.car.hvac.TemperatureControlView>
+    </LinearLayout>
 </com.android.systemui.car.systembar.CarSystemBarView>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/fan_direction.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/fan_direction.xml
index 5cbe9e7..1770796 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/fan_direction.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/fan_direction.xml
@@ -23,7 +23,7 @@
         android:orientation="horizontal">
         <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
             android:id="@+id/direction_face"
-            android:layout_width="0dp"
+            android:layout_width="@dimen/hvac_panel_airflow_button_width_1"
             android:layout_height="@dimen/hvac_panel_button_dimen"
             android:layout_weight="1"
             android:background="@drawable/hvac_default_background"
@@ -40,7 +40,7 @@
             android:layout_height="match_parent"/>
         <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
             android:id="@+id/direction_floor"
-            android:layout_width="0dp"
+            android:layout_width="@dimen/hvac_panel_airflow_button_width_1"
             android:layout_height="@dimen/hvac_panel_button_dimen"
             android:layout_weight="1"
             android:background="@drawable/hvac_default_background"
@@ -57,7 +57,7 @@
             android:layout_height="match_parent"/>
         <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
             android:id="@+id/direction_defrost_front_and_floor"
-            android:layout_width="0dp"
+            android:layout_width="@dimen/hvac_panel_airflow_button_width_1"
             android:layout_height="@dimen/hvac_panel_button_dimen"
             android:layout_weight="1"
             android:background="@drawable/hvac_default_background"
@@ -79,7 +79,7 @@
         android:orientation="horizontal">
         <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
             android:id="@+id/direction_defrost_front"
-            android:layout_width="0dp"
+            android:layout_width="@dimen/hvac_panel_airflow_button_width_2"
             android:layout_height="@dimen/hvac_panel_button_dimen"
             android:layout_weight="1"
             android:background="@drawable/hvac_default_background"
@@ -95,7 +95,7 @@
             android:layout_height="match_parent"/>
         <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
             android:id="@+id/direction_defrost_rear"
-            android:layout_width="0dp"
+            android:layout_width="@dimen/hvac_panel_airflow_button_width_2"
             android:layout_height="@dimen/hvac_panel_button_dimen"
             android:layout_weight="1"
             android:background="@drawable/hvac_default_background"
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_container.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_container.xml
index 64577aa..86df240 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_container.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_container.xml
@@ -30,35 +30,6 @@
         android:layout_height="match_parent"
         android:background="@drawable/hvac_panel_bg">
 
-        <ImageButton
-            android:id="@+id/hvac_panel_close_button"
-            android:layout_width="@dimen/hvac_panel_exit_button_dimen"
-            android:layout_height="@dimen/hvac_panel_exit_button_dimen"
-            android:layout_marginLeft="@dimen/hvac_panel_exit_button_margin"
-            android:layout_marginTop="@dimen/hvac_panel_exit_button_margin"
-            android:background="@drawable/hvac_default_background"
-            android:scaleType="center"
-            android:src="@drawable/ic_hvac_close"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintLeft_toLeftOf="parent"/>
-        <FrameLayout
-            android:id="@+id/hvac_header_title"
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/wrap_content"
-            android:layout_marginTop="@dimen/hvac_panel_title_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_dimen"
-            android:layout_marginRight="@dimen/hvac_panel_button_dimen"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintLeft_toLeftOf="parent"
-            app:layout_constraintRight_toRightOf="parent">
-            <TextView
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:textAppearance="?android:attr/textAppearanceLarge"
-                android:text="@string/hvac_panel_header"
-                android:gravity="center"/>
-        </FrameLayout>
-
         <androidx.constraintlayout.widget.Guideline
             android:id="@+id/top_guideline"
             android:layout_width="match_parent"
@@ -67,18 +38,16 @@
             app:layout_constraintGuide_begin="@dimen/hvac_panel_buttons_guideline"/>
 
         <!-- ************************ -->
-        <!-- First column of buttons. -->
+        <!-- First group of buttons. -->
         <!-- ************************ -->
 
         <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
             android:id="@+id/cooling_on_off"
             android:layout_width="@dimen/hvac_panel_button_dimen"
-            android:layout_height="@dimen/hvac_panel_button_dimen"
-            android:layout_marginBottom="@dimen/hvac_panel_button_internal_margin"
+            android:layout_height="@dimen/hvac_panel_long_button_dimen"
             android:layout_marginLeft="@dimen/hvac_panel_button_external_margin"
-            android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
             android:background="@drawable/hvac_default_background"
-            app:layout_constraintBottom_toTopOf="@+id/steering_wheel_heat_on_off"
             app:layout_constraintLeft_toLeftOf="parent"
             app:layout_constraintTop_toBottomOf="@+id/top_guideline"
             systemui:hvacAreaId="117"
@@ -87,105 +56,59 @@
             systemui:hvacToggleOnButtonDrawable="@drawable/ic_ac_on"
             systemui:hvacTurnOffIfAutoOn="true"/>
 
-        <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
-            android:id="@+id/steering_wheel_heat_on_off"
+        <com.android.systemui.car.hvac.SeatTemperatureLevelButton
+            android:id="@+id/seat_heater_driver_on_off"
             android:layout_width="@dimen/hvac_panel_button_dimen"
-            android:layout_height="@dimen/hvac_panel_button_dimen"
-            android:layout_marginBottom="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_external_margin"
-            android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
+            android:layout_height="@dimen/hvac_panel_long_button_dimen"
+            android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
             android:background="@drawable/hvac_heat_background"
-            app:layout_constraintBottom_toTopOf="@+id/hvac_on_off"
-            app:layout_constraintLeft_toLeftOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/cooling_on_off"
-            systemui:hvacAreaId="117"
-            systemui:hvacPropertyId="289408269"
-            systemui:hvacToggleOffButtonDrawable="@drawable/ic_heated_steering_off"
-            systemui:hvacToggleOnButtonDrawable="@drawable/ic_heated_steering_on"
-            systemui:hvacTurnOffIfAutoOn="true"
-            systemui:onValue="1"
-            systemui:offValue="0"/>
+            app:layout_constraintRight_toRightOf="@+id/hvac_on_off"
+            app:layout_constraintTop_toBottomOf="@+id/top_guideline"
+            systemui:hvacAreaId="1"
+            systemui:seatTemperatureType="heating"
+            systemui:seatTemperatureIconDrawableList="@array/hvac_driver_seat_heat_icons"/>
 
         <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
             android:id="@+id/hvac_on_off"
-            android:layout_width="@dimen/hvac_panel_button_dimen"
+            android:layout_width="@dimen/hvac_panel_long_button_dimen"
             android:layout_height="@dimen/hvac_panel_button_dimen"
-            android:layout_marginBottom="@dimen/hvac_panel_button_external_margin"
+            android:layout_marginBottom="@dimen/hvac_panel_button_external_bottom_margin"
             android:layout_marginLeft="@dimen/hvac_panel_button_external_margin"
-            android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
             android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
             android:background="@drawable/hvac_default_background"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintLeft_toLeftOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/steering_wheel_heat_on_off"
+            app:layout_constraintTop_toBottomOf="@+id/cooling_on_off"
             systemui:hvacAreaId="117"
             systemui:hvacPropertyId="354419984"
             systemui:hvacToggleOffButtonDrawable="@drawable/ic_power_off"
             systemui:hvacToggleOnButtonDrawable="@drawable/ic_power_on"
             systemui:hvacTurnOffIfPowerOff="false"/>
 
-        <!-- ************************* -->
-        <!-- Second column of buttons. -->
-        <!-- ************************* -->
-
-        <com.android.systemui.car.hvac.SeatTemperatureLevelButton
-            android:id="@+id/seat_cooler_driver_on_off"
-            android:layout_width="@dimen/hvac_panel_button_dimen"
-            android:layout_height="@dimen/hvac_panel_tall_button_height"
-            android:layout_marginBottom="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
-            android:background="@drawable/hvac_cool_background"
-            app:layout_constraintBottom_toTopOf="@+id/seat_heater_driver_on_off"
-            app:layout_constraintLeft_toRightOf="@+id/cooling_on_off"
-            app:layout_constraintRight_toLeftOf="@+id/airflow_group"
-            app:layout_constraintTop_toBottomOf="@+id/top_guideline"
-            systemui:hvacAreaId="1"
-            systemui:seatTemperatureType="cooling"
-            systemui:seatTemperatureIconDrawableList="@array/hvac_driver_seat_cool_icons"/>
-
-        <com.android.systemui.car.hvac.SeatTemperatureLevelButton
-            android:id="@+id/seat_heater_driver_on_off"
-            android:layout_width="@dimen/hvac_panel_button_dimen"
-            android:layout_height="@dimen/hvac_panel_tall_button_height"
-            android:layout_marginBottom="@dimen/hvac_panel_button_external_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
-            android:background="@drawable/hvac_heat_background"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintLeft_toRightOf="@+id/hvac_on_off"
-            app:layout_constraintRight_toLeftOf="@+id/fan_speed_control"
-            app:layout_constraintTop_toBottomOf="@+id/seat_cooler_driver_on_off"
-            systemui:hvacAreaId="1"
-            systemui:seatTemperatureType="heating"
-            systemui:seatTemperatureIconDrawableList="@array/hvac_driver_seat_heat_icons"/>
-
         <!-- ************************ -->
-        <!-- Third column of buttons. -->
+        <!-- Second group of buttons. -->
         <!-- ************************ -->
 
         <LinearLayout
             android:id="@+id/airflow_group"
-            android:layout_width="0dp"
+            android:layout_width="@dimen/hvac_panel_slider_width"
             android:layout_height="0dp"
-            android:layout_marginBottom="@dimen/hvac_panel_button_internal_margin"
             android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
             android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
             android:orientation="vertical"
-            app:layout_constraintBottom_toTopOf="@+id/fan_speed_control"
-            app:layout_constraintLeft_toRightOf="@+id/seat_cooler_driver_on_off"
-            app:layout_constraintRight_toLeftOf="@+id/seat_cooler_passenger_on_off"
+            app:layout_constraintLeft_toRightOf="@+id/seat_heater_driver_on_off"
+            app:layout_constraintRight_toLeftOf="@+id/seat_heater_passenger_on_off"
             app:layout_constraintTop_toBottomOf="@+id/top_guideline">
            <include layout="@layout/fan_direction"/>
         </LinearLayout>
 
         <com.android.systemui.car.hvac.custom.FanSpeedSeekBar
             android:id="@+id/fan_speed_control"
-            android:layout_width="0dp"
+            android:layout_width="@dimen/hvac_panel_slider_width"
             android:layout_height="@dimen/hvac_panel_button_dimen"
-            android:layout_marginBottom="@dimen/hvac_panel_button_external_margin"
+            android:layout_marginBottom="@dimen/hvac_panel_button_external_bottom_margin"
             android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
             android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
             android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
@@ -196,58 +119,34 @@
             android:background="@drawable/fan_speed_seek_bar_background"
             android:splitTrack="false"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintLeft_toRightOf="@+id/seat_heater_driver_on_off"
-            app:layout_constraintRight_toLeftOf="@+id/seat_heater_passenger_on_off"
-            app:layout_constraintTop_toBottomOf="@+id/airflow_windshield"/>
+            app:layout_constraintLeft_toLeftOf="@+id/airflow_group"
+            app:layout_constraintRight_toRightOf="@+id/airflow_group"
+            app:layout_constraintTop_toBottomOf="@+id/airflow_group"/>
 
         <!-- ************************* -->
-        <!-- Fourth column of buttons. -->
+        <!-- Third group of buttons. -->
         <!-- ************************* -->
 
         <com.android.systemui.car.hvac.SeatTemperatureLevelButton
-            android:id="@+id/seat_cooler_passenger_on_off"
-            android:layout_width="@dimen/hvac_panel_button_dimen"
-            android:layout_height="@dimen/hvac_panel_tall_button_height"
-            android:layout_marginBottom="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
-            android:background="@drawable/hvac_cool_background"
-            app:layout_constraintBottom_toTopOf="@+id/seat_heater_passenger_on_off"
-            app:layout_constraintRight_toLeftOf="@+id/recycle_air_on_off"
-            app:layout_constraintTop_toBottomOf="@+id/top_guideline"
-            systemui:hvacAreaId="4"
-            systemui:seatTemperatureType="cooling"
-            systemui:seatTemperatureIconDrawableList="@array/hvac_passenger_seat_cool_icons"/>
-
-        <com.android.systemui.car.hvac.SeatTemperatureLevelButton
             android:id="@+id/seat_heater_passenger_on_off"
             android:layout_width="@dimen/hvac_panel_button_dimen"
-            android:layout_height="@dimen/hvac_panel_tall_button_height"
-            android:layout_marginBottom="@dimen/hvac_panel_button_external_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
+            android:layout_height="@dimen/hvac_panel_long_button_dimen"
             android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
             android:background="@drawable/hvac_heat_background"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintRight_toLeftOf="@+id/hvac_driver_passenger_sync"
-            app:layout_constraintTop_toBottomOf="@+id/seat_cooler_passenger_on_off"
+            app:layout_constraintLeft_toLeftOf="@+id/hvac_driver_passenger_sync"
+            app:layout_constraintTop_toBottomOf="@+id/top_guideline"
             systemui:hvacAreaId="4"
             systemui:seatTemperatureType="heating"
             systemui:seatTemperatureIconDrawableList="@array/hvac_passenger_seat_heat_icons"/>
 
-        <!-- ************************ -->
-        <!-- Fifth column of buttons. -->
-        <!-- ************************ -->
-
         <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
             android:id="@+id/recycle_air_on_off"
             android:layout_width="@dimen/hvac_panel_button_dimen"
             android:layout_height="@dimen/hvac_panel_button_dimen"
-            android:layout_marginBottom="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
             android:layout_marginRight="@dimen/hvac_panel_button_external_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
             android:background="@drawable/hvac_default_background"
-            app:layout_constraintBottom_toTopOf="@+id/hvac_driver_passenger_sync"
             app:layout_constraintRight_toRightOf="parent"
             app:layout_constraintTop_toBottomOf="@+id/top_guideline"
             systemui:hvacAreaId="117"
@@ -257,42 +156,37 @@
             systemui:hvacToggleOffButtonDrawable="@drawable/ic_recirculate_off"/>
 
         <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
-            android:id="@+id/hvac_driver_passenger_sync"
+            android:id="@+id/auto_temperature_on_off"
             android:layout_width="@dimen/hvac_panel_button_dimen"
             android:layout_height="@dimen/hvac_panel_button_dimen"
-            android:layout_marginBottom="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
             android:layout_marginRight="@dimen/hvac_panel_button_external_margin"
             android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
             android:background="@drawable/hvac_default_background"
-            app:layout_constraintBottom_toTopOf="@+id/auto_temperature_on_off"
             app:layout_constraintRight_toRightOf="parent"
             app:layout_constraintTop_toBottomOf="@+id/recycle_air_on_off"
             systemui:hvacAreaId="117"
+            systemui:hvacPropertyId="354419978"
+            systemui:hvacToggleOnButtonDrawable="@drawable/ic_auto_on"
+            systemui:hvacToggleOffButtonDrawable="@drawable/ic_auto_off"/>
+
+        <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+            android:id="@+id/hvac_driver_passenger_sync"
+            android:layout_width="@dimen/hvac_panel_long_button_dimen"
+            android:layout_height="@dimen/hvac_panel_button_dimen"
+            android:layout_marginBottom="@dimen/hvac_panel_button_external_bottom_margin"
+            android:layout_marginRight="@dimen/hvac_panel_button_external_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
+            android:background="@drawable/hvac_default_background"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/auto_temperature_on_off"
+            systemui:hvacAreaId="117"
             systemui:hvacPropertyId="354419977"
             systemui:hvacToggleOffButtonDrawable="@drawable/ic_sync_off"
             systemui:hvacToggleOnButtonDrawable="@drawable/ic_sync_on"
             systemui:hvacTurnOffIfAutoOn="true"/>
 
-        <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
-            android:id="@+id/auto_temperature_on_off"
-            android:layout_width="@dimen/hvac_panel_button_dimen"
-            android:layout_height="@dimen/hvac_panel_button_dimen"
-            android:layout_marginBottom="@dimen/hvac_panel_button_external_margin"
-            android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
-            android:layout_marginRight="@dimen/hvac_panel_button_external_margin"
-            android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
-            android:background="@drawable/hvac_default_background"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/hvac_driver_passenger_sync"
-            systemui:hvacAreaId="117"
-            systemui:hvacPropertyId="354419978"
-            systemui:hvacToggleOnButtonDrawable="@drawable/ic_auto_on"
-            systemui:hvacToggleOffButtonDrawable="@drawable/ic_auto_off"/>
-
         <include
-            layout="@layout/hvac_panel_handle_bar"
-            app:layout_constraintTop_toTopOf="parent"/>
+            layout="@layout/hvac_panel_handle_bar"/>
     </com.android.systemui.car.hvac.HvacPanelView>
 </com.android.car.ui.FocusArea>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_handle_bar.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_handle_bar.xml
index 0ebd055..62a1e81 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_handle_bar.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_handle_bar.xml
@@ -14,15 +14,24 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
+
+<merge
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <View
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <FrameLayout
         android:id="@+id/handle_bar"
-        android:layout_width="@dimen/hvac_panel_handle_bar_width"
-        android:layout_height="@dimen/hvac_panel_handle_bar_height"
-        android:layout_marginTop="@dimen/hvac_panel_handle_bar_margin_top"
-        android:layout_gravity="top|center_horizontal"
-        android:background="@drawable/hvac_panel_handle_bar"/>
-</FrameLayout>
\ No newline at end of file
+        android:layout_width="@dimen/hvac_panel_handle_bar_container_width"
+        android:layout_height="@dimen/hvac_panel_handle_bar_container_height"
+        android:layout_gravity="center"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintHorizontal_chainStyle="packed">
+        <View
+            android:layout_width="@dimen/hvac_panel_handle_bar_width"
+            android:layout_height="@dimen/hvac_panel_handle_bar_height"
+            android:layout_marginTop="@dimen/hvac_panel_handle_bar_margin_top"
+            android:layout_gravity="top|center_horizontal"
+            android:background="@drawable/hvac_panel_handle_bar"/>
+    </FrameLayout>
+</merge>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/arrays.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/arrays.xml
index c1c2119..828003f 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/arrays.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/arrays.xml
@@ -19,29 +19,14 @@
     <array name="hvac_driver_seat_heat_icons">
         <item>@drawable/ic_driver_seat_heat_off</item>
         <item>@drawable/ic_driver_seat_heat_low</item>
-        <item>@drawable/ic_driver_seat_heat_med</item>
         <item>@drawable/ic_driver_seat_heat_high</item>
     </array>
     <array name="hvac_passenger_seat_heat_icons">
         <item>@drawable/ic_passenger_seat_heat_off</item>
         <item>@drawable/ic_passenger_seat_heat_low</item>
-        <item>@drawable/ic_passenger_seat_heat_med</item>
         <item>@drawable/ic_passenger_seat_heat_high</item>
     </array>
-    <array name="hvac_driver_seat_cool_icons">
-        <item>@drawable/ic_driver_seat_cool_off</item>
-        <item>@drawable/ic_driver_seat_cool_low</item>
-        <item>@drawable/ic_driver_seat_cool_med</item>
-        <item>@drawable/ic_driver_seat_cool_high</item>
-    </array>
-    <array name="hvac_passenger_seat_cool_icons">
-        <item>@drawable/ic_passenger_seat_cool_off</item>
-        <item>@drawable/ic_passenger_seat_cool_low</item>
-        <item>@drawable/ic_passenger_seat_cool_med</item>
-        <item>@drawable/ic_passenger_seat_cool_high</item>
-    </array>
     <array name="hvac_fan_speed_icons">
-        <item>@drawable/fan_speed_seek_bar_thumb_off</item>
         <item>@drawable/fan_speed_seek_bar_thumb_1</item>
         <item>@drawable/fan_speed_seek_bar_thumb_2</item>
         <item>@drawable/fan_speed_seek_bar_thumb_3</item>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/colors.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/colors.xml
index 6f685b3..8792680 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/colors.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/colors.xml
@@ -34,9 +34,12 @@
     <color name="hvac_master_switch_color">@color/car_nav_icon_fill_color</color>
     <color name="hvac_on_icon_fill_color">@android:color/black</color>
     <color name="hvac_off_icon_fill_color">@android:color/white</color>
-    <color name="hvac_on_cooling_background_color">#669DF6</color>
+    <color name="hvac_on_cooling_background_color">#6BF0FF</color>
     <color name="hvac_on_heating_background_color">#EE675C</color>
-    <color name="hvac_on_background_color">#50E3C2</color>
+    <color name="hvac_on_background_color">#6BF0FF</color>
     <color name="hvac_off_background_color">#3C4043</color>
+    <color name="hvac_panel_handle_bar_color">#3C4043</color>
 
+    <color name="dark_mode_icon_color_single_tone">@color/car_nav_icon_fill_color</color>
+    <color name="light_mode_icon_color_single_tone">@color/car_nav_icon_fill_color</color>
 </resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/config.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/config.xml
index 11cca9c..fa95f56 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/config.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/config.xml
@@ -29,5 +29,5 @@
         <item>com.android.systemui.car.userswitcher.UserSwitchTransitionViewMediator</item>
     </string-array>
 
-    <integer name="hvac_num_fan_speeds">9</integer>
+    <integer name="hvac_num_fan_speeds">8</integer>
 </resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/dimens.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/dimens.xml
index 3064fd9..8ed622b 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/dimens.xml
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/dimens.xml
@@ -42,30 +42,34 @@
     <dimen name="system_bar_user_icon_drawing_size">44dp</dimen>
     <dimen name="status_bar_system_icon_spacing">32dp</dimen>
 
+    <dimen name="hvac_panel_handle_bar_container_height">64dp</dimen>
+    <dimen name="hvac_panel_handle_bar_container_width">728dp</dimen>
     <dimen name="hvac_panel_handle_bar_height">6dp</dimen>
-    <dimen name="hvac_panel_handle_bar_margin_top">8dp</dimen>
-    <dimen name="hvac_panel_handle_bar_width">64dp</dimen>
+    <dimen name="hvac_panel_handle_bar_margin_top">17dp</dimen>
+    <dimen name="hvac_panel_handle_bar_width">120dp</dimen>
     <dimen name="hvac_panel_bg_radius">24dp</dimen>
     <dimen name="hvac_panel_off_button_radius">24dp</dimen>
-    <dimen name="hvac_panel_on_button_radius">44dp</dimen>
-    <dimen name="hvac_panel_full_expanded_height">472dp</dimen>
-    <dimen name="hvac_panel_exit_icon_dimen">21dp</dimen>
-    <dimen name="hvac_panel_exit_button_dimen">64dp</dimen>
-    <dimen name="hvac_panel_exit_button_margin">24dp</dimen>
+    <dimen name="hvac_panel_on_button_radius">24dp</dimen>
+    <dimen name="hvac_panel_seek_bar_radius">44dp</dimen>
+    <dimen name="hvac_panel_full_expanded_height">456dp</dimen>
     <dimen name="hvac_panel_title_margin">36dp</dimen>
-    <dimen name="hvac_panel_buttons_guideline">120dp</dimen>
+    <dimen name="hvac_panel_buttons_guideline">@dimen/hvac_panel_handle_bar_container_height</dimen>
+
     <dimen name="hvac_panel_icon_dimen">48dp</dimen>
     <dimen name="hvac_panel_tall_icon_dimen">108dp</dimen>
     <dimen name="hvac_panel_wide_icon_dimen">96dp</dimen>
-    <dimen name="hvac_panel_icon_inset_dimen">22dp</dimen>
-    <dimen name="hvac_panel_icon_inset_wide_dimen">92dp</dimen>
-    <dimen name="hvac_panel_icon_inset_extra_wide_dimen">140dp</dimen>
+
     <dimen name="hvac_panel_button_dimen">88dp</dimen>
-    <dimen name="hvac_panel_tall_button_height">148dp</dimen>
-    <dimen name="hvac_panel_wide_button_width">374dp</dimen>
+    <dimen name="hvac_panel_long_button_dimen">208dp</dimen>
+    <dimen name="hvac_panel_airflow_button_width_1">210dp</dimen>
+    <dimen name="hvac_panel_airflow_button_width_2">332dp</dimen>
     <dimen name="hvac_panel_slider_width">696dp</dimen>
+
     <dimen name="hvac_panel_button_external_margin">24dp</dimen>
-    <dimen name="hvac_panel_button_internal_margin">16dp</dimen>
+    <dimen name="hvac_panel_button_external_top_margin">16dp</dimen>
+    <dimen name="hvac_panel_button_external_bottom_margin">48dp</dimen>
+    <dimen name="hvac_panel_button_internal_margin">32dp</dimen>
+
     <item name="hvac_heat_or_cool_off_alpha" format="float" type="dimen">0.3</item>
 
     <dimen name="toast_margin">24dp</dimen>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/integers.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/integers.xml
new file mode 100644
index 0000000..282373c
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/integers.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ 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.
+  -->
+
+<resources>
+    <integer name="hvac_seat_heat_level_count">3</integer>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/custom/FanSpeedSeekBar.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/custom/FanSpeedSeekBar.java
index bf409a3..bc7f4f2 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/custom/FanSpeedSeekBar.java
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/custom/FanSpeedSeekBar.java
@@ -106,12 +106,15 @@
 
     private void init(AttributeSet attrs) {
         int speeds = mContext.getResources().getInteger(R.integer.hvac_num_fan_speeds);
-        setMin(0);
+        if (speeds < 1) {
+            throw new IllegalArgumentException("The nuer of fan speeds should be > 1");
+        }
+
+        setMin(1);
         incrementProgressBy(1);
-        // Subtract 1 since we're starting from 0.
-        setMax(speeds - 1);
+        setMax(speeds);
         int thumbRadius = mContext.getResources().getDimensionPixelSize(
-                R.dimen.hvac_panel_on_button_radius);
+                R.dimen.hvac_panel_seek_bar_radius);
         setPadding(thumbRadius, 0, thumbRadius, 0);
         mHvacGlobalAreaId = mContext.getResources().getInteger(R.integer.hvac_global_area_id);
 
@@ -136,7 +139,7 @@
         }
 
         for (int i = 0; i < speeds; i++) {
-            mIcons.set(i, fanSpeedThumbIcons.getDrawable(i));
+            mIcons.set(i + 1, fanSpeedThumbIcons.getDrawable(i));
         }
         fanSpeedThumbIcons.recycle();
         typedArray.recycle();
diff --git a/car_product/car_ui_portrait/apps/HideApps/Android.mk b/car_product/car_ui_portrait/apps/HideApps/Android.mk
index 3dc4adf..74cdcaa 100644
--- a/car_product/car_ui_portrait/apps/HideApps/Android.mk
+++ b/car_product/car_ui_portrait/apps/HideApps/Android.mk
@@ -29,4 +29,6 @@
     RotaryIME \
     CarRotaryImeRRO \
 
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
 include $(BUILD_PACKAGE)
diff --git a/car_product/car_ui_portrait/bootanimation/bootanimation.zip b/car_product/car_ui_portrait/bootanimation/bootanimation.zip
index cc40a4b..d7b90b3 100644
--- a/car_product/car_ui_portrait/bootanimation/bootanimation.zip
+++ b/car_product/car_ui_portrait/bootanimation/bootanimation.zip
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/desc.txt b/car_product/car_ui_portrait/bootanimation/parts/desc.txt
index 483f8e2..a4ebbac 100644
--- a/car_product/car_ui_portrait/bootanimation/parts/desc.txt
+++ b/car_product/car_ui_portrait/bootanimation/parts/desc.txt
@@ -1,2 +1,2 @@
 986 1000 60
-p 0 0 part0
+c 1 0 part0
diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/Android.bp b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp
similarity index 66%
rename from packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/Android.bp
rename to car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp
index 6ce5d26..2a4357c 100644
--- a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/Android.bp
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp
@@ -11,25 +11,20 @@
 // 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 {
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
-
-cc_library_shared {
-    name: "libscriptexecutorjniutils-test",
-
-    defaults: [
-        "scriptexecutor_defaults",
+android_app {
+    name: "CarEvsCameraPreviewAppRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    certificate: "platform",
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
     ],
-
-    srcs: [
-        "JniUtilsTestHelper.cpp",
-    ],
-
-    shared_libs: [
-        "libnativehelper",
-        "libscriptexecutor",
+    static_libs: [
+        "androidx-constraintlayout_constraintlayout",
+        "androidx-constraintlayout_constraintlayout-solver",
     ],
 }
-
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/AndroidManifest.xml
new file mode 100644
index 0000000..c878ee1
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.google.android.car.evs.caruiportrait.rro">
+    <application android:hasCode="false"/>
+    <overlay android:priority="20"
+             android:targetName="CarEvsCameraPreviewApp"
+             android:targetPackage="com.google.android.car.evs"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/close_bg.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/close_bg.xml
new file mode 100644
index 0000000..9a4596b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/close_bg.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="?android:attr/colorBackground"/>
+    <corners android:radius="@dimen/close_button_radius"/>
+</shape>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_hvac_close.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.xml
similarity index 85%
rename from car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_hvac_close.xml
rename to car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.xml
index e9ae42e..5874541 100644
--- a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_hvac_close.xml
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.xml
@@ -15,11 +15,11 @@
   ~ limitations under the License.
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="@dimen/hvac_panel_exit_icon_dimen"
-        android:height="@dimen/hvac_panel_exit_icon_dimen"
+        android:width="@dimen/close_icon_dimen"
+        android:height="@dimen/close_icon_dimen"
         android:viewportWidth="26"
         android:viewportHeight="26">
     <path
         android:pathData="M25.8327 2.75199L23.2477 0.166992L12.9994 10.4153L2.75102 0.166992L0.166016 2.75199L10.4144 13.0003L0.166016 23.2487L2.75102 25.8337L12.9994 15.5853L23.2477 25.8337L25.8327 23.2487L15.5844 13.0003L25.8327 2.75199Z"
-        android:fillColor="@color/hvac_off_icon_fill_color" />
+        android:fillColor="?android:attr/textColorPrimary"/>
 </vector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/layout/evs_preview_activity.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/layout/evs_preview_activity.xml
new file mode 100644
index 0000000..bfd6370
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/layout/evs_preview_activity.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/transparent">
+    <LinearLayout
+        android:id="@+id/evs_preview_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/transparent"
+        android:orientation="vertical"/>
+
+    <ImageButton
+        android:id="@+id/close_button"
+        android:layout_width="@dimen/close_button_dimen"
+        android:layout_height="@dimen/close_button_dimen"
+        android:layout_marginLeft="@dimen/close_button_margin"
+        android:layout_marginTop="@dimen/close_button_margin"
+        android:background="@drawable/close_bg"
+        android:scaleType="center"
+        android:alpha="0.5"
+        android:src="@drawable/ic_close"/>
+</FrameLayout>
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/config.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/config.xml
new file mode 100644
index 0000000..935c096
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/config.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.
+  -->
+<resources>
+    <!-- Shade of the background behind the camera window. 1.0 for fully opaque, 0.0 for fully
+         transparent. -->
+    <item name="config_cameraBackgroundScrim" format="float" type="dimen">0.7</item>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/dimens.xml
new file mode 100644
index 0000000..ea3ce97
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ 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.
+  -->
+<resources>
+    <!-- dimensions for evs camera preview in the system window -->
+    <dimen name="camera_preview_width">1224dp</dimen>
+    <dimen name="camera_preview_height">720dp</dimen>
+
+    <dimen name="close_icon_dimen">28dp</dimen>
+    <dimen name="close_button_dimen">80dp</dimen>
+    <dimen name="close_button_margin">24dp</dimen>
+    <dimen name="close_button_radius">20dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/strings.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/strings.xml
new file mode 100644
index 0000000..77cc4d1
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<resources>
+    <string name="app_name">Rearview Camera</string>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..681ef74
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/xml/overlays.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<overlay>
+    <item target="layout/evs_preview_activity" value="@layout/evs_preview_activity"/>
+
+    <item target="string/app_name" value="@string/app_name"/>
+
+    <item target="dimen/camera_preview_width" value="@dimen/camera_preview_width"/>
+    <item target="dimen/camera_preview_height" value="@dimen/camera_preview_height"/>
+
+    <item target="id/evs_preview_container" value="@id/evs_preview_container"/>
+    <item target="id/close_button" value="@id/close_button"/>
+
+    <item target="dimen/config_cameraBackgroundScrim" value="@dimen/config_cameraBackgroundScrim"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp
index 051115c..7e3df0a 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp
@@ -24,6 +24,7 @@
     ],
     static_libs: [
         "androidx.cardview_cardview",
+        "androidx-constraintlayout_constraintlayout",
         "car-media-common",
         "car-apps-common",
     ],
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/default_audio_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/default_audio_background.xml
new file mode 100644
index 0000000..f00f03b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/default_audio_background.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  ~ 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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:bottom="@dimen/default_audio_icon_padding"
+          android:top="@dimen/default_audio_icon_padding"
+          android:right="@dimen/default_audio_icon_padding"
+          android:left="@dimen/default_audio_icon_padding">
+        <shape android:shape="oval">
+            <stroke
+                android:width="@dimen/default_audio_icon_outer_ring_thickness"
+                android:color="@color/default_audio_background_image_color"/>
+            <size
+                android:width="@dimen/default_audio_icon_outer_ring_size"
+                android:height="@dimen/default_audio_icon_outer_ring_size"/>
+        </shape>
+    </item>
+    <item android:gravity="center"
+          android:drawable="@drawable/ic_play_music"/>
+</layer-list>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/ic_play_music.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/ic_play_music.xml
new file mode 100644
index 0000000..91190e7
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/ic_play_music.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+  ~ 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.
+  -->
+
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/default_audio_icon_inner_icon_size"
+    android:height="@dimen/default_audio_icon_inner_icon_size"
+    android:viewportWidth="40"
+    android:viewportHeight="40">
+    <path
+        android:pathData="M40,20C40,8.96 31.04,0 20,0C8.96,0 0,8.96 0,20C0,31.04 8.96,40 20,40C31.04,40 40,31.04 40,20ZM28,14H22V25C22,27.76 19.76,30 17,30C14.24,30 12,27.76 12,25C12,22.24 14.24,20 17,20C18.14,20 19.16,20.38 20,21.02V10H28V14Z"
+        android:fillColor="@color/default_audio_background_image_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_media.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_media.xml
index 34fe61d..932a22f 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_media.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_media.xml
@@ -28,18 +28,19 @@
              android:layout_height="match_parent"
              android:layout_width="0dp"
              android:layout_weight="1"
-             android:layout_gravity="start|center_vertical"/>
+             android:layout_gravity="start"/>
 
     <FrameLayout
         android:layout_height="match_parent"
         android:layout_width="0dp"
         android:layout_weight="1"
-        android:layout_gravity="end|center_vertical">
+        android:layout_gravity="end">
 
-        <com.android.car.media.common.PlaybackControlsActionBar
-            android:id="@+id/media_playback_controls_bar"
-            android:layout_height="match_parent"
-            android:layout_width="match_parent"
-            app:columns="@integer/playback_controls_bar_columns"/>
+    <com.android.car.media.common.PlaybackControlsActionBar
+        android:id="@+id/media_playback_controls_bar"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        app:enableOverflow="true"
+        app:columns="@integer/playback_controls_bar_columns"/>
     </FrameLayout>
 </LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_fragment.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_fragment.xml
index ccc8b80..48efb6f 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_fragment.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_fragment.xml
@@ -27,39 +27,34 @@
         android:id="@+id/card_background"
         android:visibility="gone"
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
-
-        <!-- card_background_image is required by HomeCardFragment. Intentionally not shown by
-         setting visibility="gone" -->
-        <com.android.car.apps.common.CrossfadeImageView
-            android:id="@+id/card_background_image"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:visibility="gone"
-            android:scaleType="centerCrop"/>
-
-        <View
-            android:id="@+id/card_background_scrim"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:focusable="false"
-            android:background="@color/card_background_scrim"
-            android:alpha="@dimen/card_background_scrim_alpha"/>
-    </FrameLayout>
+        android:layout_height="match_parent"/>
 
     <RelativeLayout
         android:layout_height="match_parent"
         android:layout_width="match_parent">
 
-        <ImageView
-            android:id="@+id/card_icon"
+        <FrameLayout
+            android:id="@+id/control_bar_image_container"
             android:layout_height="@dimen/control_bar_image_size"
             android:layout_width="@dimen/control_bar_image_size"
-            android:layout_marginStart="@dimen/card_icon_margin_start"
-            android:layout_alignParentStart="true"
-            android:layout_centerVertical="true"/>
+            android:layout_marginStart="@dimen/card_icon_margin_start">
 
-        <!-- Do not show -->
+            <com.android.car.apps.common.CrossfadeImageView
+                android:id="@+id/card_background_image"
+                android:layout_height="match_parent"
+                android:layout_width="match_parent"/>
+
+            <ImageView
+                android:id="@+id/card_icon"
+                android:layout_height="@dimen/control_bar_app_icon_size"
+                android:layout_width="@dimen/control_bar_app_icon_size"
+                android:layout_gravity="bottom|end"
+                android:layout_marginEnd="@dimen/control_bar_app_icon_margin"
+                android:layout_marginBottom="@dimen/control_bar_app_icon_margin"
+                android:scaleType="centerInside"/>
+        </FrameLayout>
+
+        <!-- Do not show app name -->
         <TextView
             android:id="@+id/card_name"
             android:layout_height="match_parent"
@@ -71,8 +66,7 @@
         <FrameLayout
             android:layout_height="match_parent"
             android:layout_width="0dp"
-            android:layout_centerVertical="true"
-            android:layout_toEndOf="@id/card_name"
+            android:layout_toEndOf="@id/control_bar_image_container"
             android:layout_alignParentEnd="true"
             android:layout_marginStart="@dimen/card_content_margin_start">
 
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/colors.xml
index 0604222..a3a7f24 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/colors.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/colors.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8" ?>
+<?xml version="1.0" encoding="UTF-8" ?>
 <!--
   ~ Copyright (C) 2021 The Android Open Source Project
   ~
@@ -16,5 +16,5 @@
   -->
 
 <resources>
-    <color name="card_background_scrim">#000000</color>
+    <color name="default_audio_background_image_color">#515355</color>
 </resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/config.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/config.xml
index afc5169..c912ab9 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/config.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/config.xml
@@ -21,4 +21,8 @@
         <item>com.android.car.carlauncher.homescreen.audio.AudioCard</item>
     </string-array>
 
+    <string-array name="config_foregroundDAComponents" translatable="false">
+        <item>com.android.car.carlauncher/.AppGridActivity</item>
+        <item>com.android.car.notification/.CarNotificationCenterActivity</item>
+    </string-array>
 </resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/dimens.xml
index 3d7c94d..d90de2d 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/dimens.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/dimens.xml
@@ -15,18 +15,19 @@
   ~ limitations under the License.
   -->
 <resources>
-    <dimen name="card_icon_margin_start">24dp</dimen>
-    <dimen name="card_content_margin_start">32dp</dimen>
+    <dimen name="card_icon_margin_start">8dp</dimen>
+    <dimen name="card_content_margin_start">16dp</dimen>
 
-    <dimen name="descriptive_text_vertical_margin">22dp</dimen>
+    <dimen name="descriptive_text_vertical_margin">16dp</dimen>
 
-    <dimen name="control_bar_image_size">56dp</dimen>
+    <dimen name="control_bar_image_size">120dp</dimen>
+    <dimen name="control_bar_app_icon_size">36dp</dimen>
+    <dimen name="control_bar_app_icon_margin">6dp</dimen>
 
-    <dimen name="control_bar_action_icon_size">56dp</dimen>
+    <dimen name="control_bar_action_icon_size">88dp</dimen>
 
-    <!-- Percent transparency of the scrim applied to the image used for the card's background as a float between 0 and 1, where 0 applies no darkening scrim-->
-    <dimen name="card_background_scrim_alpha" format="float">0.72</dimen>
-
+    <!--Percent by which to blur the image used for the card's background as a float between 0 and 1, where 0 is not blurred-->
+    <dimen name="card_background_image_blur_radius" format="float">0</dimen>
 
     <!-- screen height of the device. This is used when custom policy is provided using
     config_deviceSpecificDisplayAreaPolicyProvider -->
@@ -41,4 +42,9 @@
     <dimen name="title_bar_display_area_height">40dp</dimen>
     <!-- This value is 500dp + (top of title bar)  -->
     <dimen name="title_bar_display_area_touch_drag_threshold">1204dp</dimen>
+
+    <dimen name="default_audio_icon_padding">26dp</dimen>
+    <dimen name="default_audio_icon_outer_ring_size">66dp</dimen>
+    <dimen name="default_audio_icon_outer_ring_thickness">8dp</dimen>
+    <dimen name="default_audio_icon_inner_icon_size">40dp</dimen>
 </resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/xml/overlays.xml
index 52cfdda..9cc1245 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/xml/overlays.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/xml/overlays.xml
@@ -17,7 +17,6 @@
     <item target="id/bottom_card" value="@id/bottom_card" />
     <item target="id/card_background" value="@id/card_background" />
     <item target="id/card_background_image" value="@id/card_background_image" />
-    <item target="id/card_background_scrim" value="@id/card_background_scrim" />
     <item target="id/card_icon" value="@id/card_icon" />
     <item target="id/card_name" value="@id/card_name" />
     <item target="id/card_view" value="@id/card_view" />
@@ -43,6 +42,7 @@
     <item target="layout/car_launcher" value="@layout/car_launcher"/>
     <item target="layout/descriptive_text" value="@layout/descriptive_text" />
 
+    <item target="dimen/card_background_image_blur_radius" value="@dimen/card_background_image_blur_radius" />
     <item target="dimen/total_screen_height" value="@dimen/total_screen_height"/>
     <item target="dimen/total_screen_width" value="@dimen/total_screen_width"/>
     <item target="dimen/control_bar_height" value="@dimen/control_bar_height"/>
@@ -50,8 +50,9 @@
     <item target="dimen/title_bar_display_area_height" value="@dimen/title_bar_display_area_height"/>
     <item target="dimen/title_bar_display_area_touch_drag_threshold" value="@dimen/title_bar_display_area_touch_drag_threshold"/>
 
+    <item target="drawable/default_audio_background" value="@drawable/default_audio_background"/>
     <item target="integer/enter_exit_animation_foreground_display_area_duration_ms" value="@integer/enter_exit_animation_foreground_display_area_duration_ms"/>
 
     <item target="array/config_homeCardModuleClasses" value="@array/config_homeCardModuleClasses"/>
-
+    <item target="array/config_foregroundDAComponents" value="@array/config_foregroundDAComponents"/>
 </overlay>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/drawable/image_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/drawable/image_background.xml
new file mode 100644
index 0000000..e5f9fa5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/drawable/image_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/image_radius"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/dimens.xml
new file mode 100644
index 0000000..9726095
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <dimen name="image_radius">24dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/styles.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/styles.xml
new file mode 100644
index 0000000..dbc8eec
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <style name="MediaIconContainerStyle">
+        <item name="android:background">@drawable/image_background</item>
+        <item name="android:clipToOutline">true</item>
+    </style>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/xml/overlays.xml
index 744f5c1..5846fa3 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/xml/overlays.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/xml/overlays.xml
@@ -17,4 +17,6 @@
 <overlay>
     <item target="bool/show_mini_playback_controls" value="@bool/show_mini_playback_controls" />
     <item target="bool/switch_to_playback_view_when_playable_item_is_clicked" value="@bool/switch_to_playback_view_when_playable_item_is_clicked" />
+
+    <item target="style/MediaIconContainerStyle" value="@style/MediaIconContainerStyle" />
 </overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_notification_actions_view.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_notification_actions_view.xml
index c3e5db0..a3c2ce3 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_notification_actions_view.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_notification_actions_view.xml
@@ -22,7 +22,7 @@
         android:layout_height="wrap_content"
         android:orientation="horizontal">
 
-        <Button
+        <com.android.car.notification.template.CarNotificationActionButton
             android:id="@+id/action_1"
             style="@style/NotificationActionButton1"
             android:layout_width="0dp"
@@ -33,7 +33,7 @@
             android:paddingTop="@dimen/action_button_padding_top"
             android:visibility="gone"/>
 
-        <Button
+        <com.android.car.notification.template.CarNotificationActionButton
             android:id="@+id/action_2"
             style="@style/NotificationActionButton2"
             android:layout_weight="1"
@@ -45,7 +45,7 @@
             android:paddingTop="@dimen/action_button_padding_top"
             android:visibility="gone"/>
 
-        <Button
+        <com.android.car.notification.template.CarNotificationActionButton
             android:id="@+id/action_3"
             style="@style/NotificationActionButton3"
             android:layout_width="0dp"
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/message_headsup_notification_template.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/message_headsup_notification_template.xml
index efa3b57..18f3304 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/message_headsup_notification_template.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/message_headsup_notification_template.xml
@@ -34,21 +34,12 @@
             android:layout_height="wrap_content"
             android:layout_marginTop="@dimen/car_notification_card_inner_top_margin">
 
-            <ImageView
-                android:id="@+id/notification_body_icon"
-                android:layout_width="@dimen/notification_touch_target_size"
-                android:layout_height="@dimen/notification_touch_target_size"
-                android:layout_alignParentStart="true"
-                android:layout_alignParentTop="true"
-                android:layout_marginStart="@dimen/card_start_margin"/>
-
             <com.android.car.notification.template.CarNotificationHeaderView
                 android:id="@+id/notification_header"
                 android:layout_width="0dp"
                 android:layout_height="0dp"
                 android:layout_alignParentTop="true"
-                android:layout_toEndOf="@id/notification_body_icon"
-                android:layout_alignParentEnd="true"
+                android:layout_alignParentStart="true"
                 app:isHeadsUp="true"/>
 
             <com.android.car.notification.template.CarNotificationBodyView
@@ -57,20 +48,12 @@
                 android:layout_height="wrap_content"
                 android:minHeight="@dimen/notification_touch_target_size"
                 android:gravity="center_vertical"
-                android:layout_toEndOf="@id/notification_body_icon"
                 android:layout_alignParentTop="true"
+                android:layout_alignParentStart="true"
                 android:layout_toStartOf="@id/message_count"
                 android:layout_marginStart="@dimen/card_body_margin_start"
                 app:maxLines="@integer/config_headsUpNotificationMaxBodyLines"
-                app:showBigIcon="false"/>
-
-            <TextView
-                android:id="@+id/message_count"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_above="@id/notification_actions_wrapper"
-                android:layout_alignParentEnd="true"
-                android:layout_marginEnd="@dimen/card_end_margin"/>
+                app:showBigIcon="true"/>
 
             <FrameLayout
                 android:id="@+id/notification_actions_wrapper"
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/notification_center_activity.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/notification_center_activity.xml
new file mode 100644
index 0000000..83a6525
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/notification_center_activity.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  ~ 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.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.android.car.ui.FocusParkingView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <com.android.car.ui.FocusArea
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.android.car.notification.CarNotificationView
+            android:id="@+id/notification_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <FrameLayout
+                android:id="@+id/exit_button_container"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:visibility="gone"/>
+
+            <TextView
+                android:id="@+id/empty_notification_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                app:layout_constraintBottom_toTopOf="@id/manage_button"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintVertical_chainStyle="packed"
+                android:text="@string/empty_notification_header"
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:visibility="gone"/>
+
+            <Button
+                android:id="@+id/manage_button"
+                style="@style/ManageButton"
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/manage_button_height"
+                android:layout_marginTop="@dimen/manage_button_top_margin"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/empty_notification_text"
+                app:layout_constraintVertical_chainStyle="packed"
+                android:text="@string/manage_text"
+                android:visibility="gone"/>
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/notifications"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:orientation="vertical"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/notification_center_title"/>
+        </com.android.car.notification.CarNotificationView>
+    </com.android.car.ui.FocusArea>
+</FrameLayout>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/config.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/config.xml
index e6391ee..4862022 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/config.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/config.xml
@@ -21,4 +21,16 @@
     <string name="config_headsUpNotificationAnimationHelper" translatable="false">
         com.android.car.notification.headsup.animationhelper.CarHeadsUpNotificationBottomAnimationHelper</string>
 
+    <!-- If false, small icon will be used to distinguish the app, large icon will be used
+         in notification body and notification header will be shown.-->
+    <bool name="config_useLauncherIcon">false</bool>
+
+    <!-- Whether to show header for the notifications center -->
+    <bool name="config_showHeaderForNotifications">true</bool>
+
+    <!-- Whether to show footer for the notifications center -->
+    <bool name="config_showFooterForNotifications">false</bool>
+
+    <!-- Whether to show Recents/Older header for notifications list -->
+    <bool name="config_showRecentAndOldHeaders">false</bool>
 </resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/strings.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/strings.xml
index dd53e12..981ffdc 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/strings.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/strings.xml
@@ -18,4 +18,7 @@
 <resources>
     <!-- The assistant action label to read aloud a message notification and optionally prompt user to respond [CHAR_LIMIT=20]-->
     <string name="assist_action_play_label">Play message</string>
+
+    <!-- Notification header text displayed on top of the notification center shade [CHAR_LIMIT=25] -->
+    <string name="notification_header">Notification Center</string>
 </resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/xml/overlays.xml
index 9e2ca19..b768ecb 100644
--- a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/xml/overlays.xml
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/xml/overlays.xml
@@ -16,11 +16,16 @@
   -->
 
 <overlay>
-    <item target="attr/isHeadsUp" value="@attr/isHeadsUp" />
-    <item target="attr/maxLines" value="@attr/maxLines" />
-    <item target="attr/showBigIcon" value="@attr/showBigIcon" />
+    <item target="attr/cardCornerRadius" value="@attr/cardCornerRadius"/>
+    <item target="attr/isHeadsUp" value="@attr/isHeadsUp"/>
+    <item target="attr/maxLines" value="@attr/maxLines"/>
+    <item target="attr/showBigIcon" value="@attr/showBigIcon"/>
 
     <item target="bool/config_showHeadsUpNotificationOnBottom" value="@bool/config_showHeadsUpNotificationOnBottom" />
+    <item target="bool/config_useLauncherIcon" value="@bool/config_useLauncherIcon"/>
+    <item target="bool/config_showHeaderForNotifications" value="@bool/config_showHeaderForNotifications"/>
+    <item target="bool/config_showFooterForNotifications" value="@bool/config_showFooterForNotifications"/>
+    <item target="bool/config_showRecentAndOldHeaders" value="@bool/config_showRecentAndOldHeaders"/>
 
     <item target="dimen/action_button_height" value="@dimen/action_button_height" />
     <item target="dimen/action_button_radius" value="@dimen/action_button_radius" />
@@ -41,27 +46,31 @@
     <item target="id/card_view" value="@id/card_view" />
     <item target="id/headsup_content" value="@id/headsup_content"/>
     <item target="id/inner_template_view" value="@id/inner_template_view" />
-    <item target="id/message_count" value="@id/message_count"/>
     <item target="id/notification_actions" value="@id/notification_actions" />
     <item target="id/notification_actions_wrapper" value="@id/notification_actions_wrapper" />
     <item target="id/notification_body" value="@id/notification_body"/>
-    <item target="id/notification_body_icon" value="@id/notification_body_icon" />
     <item target="id/notification_header" value="@id/notification_header"/>
     <item target="id/notification_headsup" value="@id/notification_headsup"/>
+    <item target="id/notification_view" value="@id/notification_view"/>
+    <item target="id/notifications" value="@id/notifications"/>
+    <item target="id/manage_button" value="@id/manage_button"/>
+    <item target="id/empty_notification_text" value="@id/empty_notification_text"/>
+    <item target="id/exit_button_container" value="@id/exit_button_container"/>
 
     <item target="layout/car_notification_actions_view" value="@layout/car_notification_actions_view"/>
     <item target="layout/headsup_container_bottom" value="@layout/headsup_container_bottom"/>
     <item target="layout/message_headsup_notification_template" value="@layout/message_headsup_notification_template" />
+    <item target="layout/notification_center_activity" value="@layout/notification_center_activity"/>
 
     <item target="string/assist_action_play_label" value="@string/assist_action_play_label"/>
     <item target="string/config_headsUpNotificationAnimationHelper" value="@string/config_headsUpNotificationAnimationHelper" />
+    <item target="string/notification_header" value="@string/notification_header"/>
 
     <item target="style/NotificationActionButtonBase" value="@style/NotificationActionButtonBase"/>
     <item target="style/NotificationActionViewLayout" value="@style/NotificationActionViewLayout"/>
     <item target="style/NotificationBodContentText" value="@style/NotificationBodyContentText" />
     <item target="style/NotificationBodyTitleText" value="@style/NotificationBodyTitleText" />
 
-
     <item target="dimen/card_min_bottom_padding" value="@dimen/card_min_bottom_padding"/>
     <item target="dimen/card_min_top_padding" value="@dimen/card_min_top_padding"/>
 </overlay>
diff --git a/car_product/car_ui_portrait/rro/android/Android.bp b/car_product/car_ui_portrait/rro/android/Android.bp
index 86739b8..73878ea 100644
--- a/car_product/car_ui_portrait/rro/android/Android.bp
+++ b/car_product/car_ui_portrait/rro/android/Android.bp
@@ -12,6 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 runtime_resource_overlay {
     name: "CarUiPortraitFrameworkResRRO",
     resource_dirs: ["res"],
@@ -29,4 +33,4 @@
         "--no-resource-deduping",
         "--no-resource-removal"
     ],
-}
\ No newline at end of file
+}
diff --git a/car_product/car_ui_portrait/rro/android/res/anim/fade_in.xml b/car_product/car_ui_portrait/rro/android/res/anim/fade_in.xml
new file mode 100644
index 0000000..bff68d0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/anim/fade_in.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:interpolator="@android:interpolator/decelerate_quad">
+
+    <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+        android:duration="@android:integer/config_longAnimTime"/>
+</set>
diff --git a/car_product/car_ui_portrait/rro/android/res/anim/fade_out.xml b/car_product/car_ui_portrait/rro/android/res/anim/fade_out.xml
new file mode 100644
index 0000000..b1b60db
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/anim/fade_out.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:interpolator="@android:interpolator/decelerate_quad">
+
+    <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+        android:duration="@android:integer/config_longAnimTime"/>
+</set>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/btn_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/btn_device_default_dark.xml
index 4fac96d..037fe3e 100644
--- a/car_product/car_ui_portrait/rro/android/res/color/btn_device_default_dark.xml
+++ b/car_product/car_ui_portrait/rro/android/res/color/btn_device_default_dark.xml
@@ -18,6 +18,6 @@
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:state_enabled="false"
           android:alpha="?android:attr/disabledAlpha"
-          android:color="@*android:color/system_neutral1_200"/>
-    <item android:color="@*android:color/system_neutral1_200"/>
+          android:color="@*android:color/system_accent1_200"/>
+    <item android:color="@*android:color/system_accent1_200"/>
 </selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/android/res/drawable/btn_borderless_car.xml b/car_product/car_ui_portrait/rro/android/res/drawable/btn_borderless_car.xml
index e5392ee..0693426 100644
--- a/car_product/car_ui_portrait/rro/android/res/drawable/btn_borderless_car.xml
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/btn_borderless_car.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- 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.
diff --git a/car_product/car_ui_portrait/rro/android/res/values/colors.xml b/car_product/car_ui_portrait/rro/android/res/values/colors.xml
index b02d7be..16f1e2b 100644
--- a/car_product/car_ui_portrait/rro/android/res/values/colors.xml
+++ b/car_product/car_ui_portrait/rro/android/res/values/colors.xml
@@ -16,6 +16,8 @@
   -->
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
   <color name="car_alert_dialog_action_button_color">#2E3134</color>
+  <color name="car_card_ripple_background_dark">?android:attr/colorControlHighlight</color>
+  <color name="car_card_ripple_background_light">?android:attr/colorControlHighlight</color>
 
   <color name="system_accent1_0">#ffffff</color>
   <!--  duped-->
@@ -63,4 +65,5 @@
   <color name="error_color_device_default_dark">#ec928e</color> <!-- Material Red 300 -->
   <color name="error_color_device_default_light">#b3261e</color> <!-- Material Red 600 -->
 
+  <color name="list_divider_color">#2E3134</color>
 </resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/styles.xml b/car_product/car_ui_portrait/rro/android/res/values/styles.xml
index 5149b83..c32411b 100644
--- a/car_product/car_ui_portrait/rro/android/res/values/styles.xml
+++ b/car_product/car_ui_portrait/rro/android/res/values/styles.xml
@@ -34,4 +34,38 @@
         <item name="android:lineHeight">36sp</item>
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
+
+    <!-- Override the default activity transitions. We have to do a full copy and not just inherit
+         and override because we're replacing the default style across the system.
+    -->
+    <style name="Animation.Activity" parent="*android:Animation.Material.Activity">
+        <item name="android:activityOpenEnterAnimation">@*android:anim/fade_in</item>
+        <item name="android:activityOpenExitAnimation">@*android:anim/fade_out</item>
+        <item name="android:activityCloseEnterAnimation">@*android:anim/fade_in</item>
+        <item name="android:activityCloseExitAnimation">@*android:anim/fade_out</item>
+        <item name="android:taskOpenEnterAnimation">@*android:anim/fade_in</item>
+        <item name="android:taskOpenExitAnimation">@*android:anim/fade_out</item>
+        <item name="android:launchTaskBehindTargetAnimation">@*android:anim/launch_task_behind_target</item>
+        <item name="android:launchTaskBehindSourceAnimation">@*android:anim/launch_task_behind_source</item>
+        <item name="android:taskCloseEnterAnimation">@*android:anim/fade_in</item>
+        <item name="android:taskCloseExitAnimation">@*android:anim/fade_out</item>
+        <item name="android:taskToFrontEnterAnimation">@*android:anim/fade_in</item>
+        <item name="android:taskToFrontExitAnimation">@*android:anim/fade_out</item>
+        <item name="android:taskToBackEnterAnimation">@*android:anim/task_close_enter</item>
+        <item name="android:taskToBackExitAnimation">@*android:anim/task_close_exit</item>
+        <item name="android:wallpaperOpenEnterAnimation">@*android:anim/wallpaper_open_enter</item>
+        <item name="android:wallpaperOpenExitAnimation">@*android:anim/wallpaper_open_exit</item>
+        <item name="android:wallpaperCloseEnterAnimation">@*android:anim/wallpaper_close_enter</item>
+        <item name="android:wallpaperCloseExitAnimation">@*android:anim/wallpaper_close_exit</item>
+        <item name="android:wallpaperIntraOpenEnterAnimation">@*android:anim/wallpaper_intra_open_enter</item>
+        <item name="android:wallpaperIntraOpenExitAnimation">@*android:anim/wallpaper_intra_open_exit</item>
+        <item name="android:wallpaperIntraCloseEnterAnimation">@*android:anim/wallpaper_intra_close_enter</item>
+        <item name="android:wallpaperIntraCloseExitAnimation">@*android:anim/wallpaper_intra_close_exit</item>
+        <item name="android:fragmentOpenEnterAnimation">@*android:animator/fragment_open_enter</item>
+        <item name="android:fragmentOpenExitAnimation">@*android:animator/fragment_open_exit</item>
+        <item name="android:fragmentCloseEnterAnimation">@*android:animator/fragment_close_enter</item>
+        <item name="android:fragmentCloseExitAnimation">@*android:animator/fragment_close_exit</item>
+        <item name="android:fragmentFadeEnterAnimation">@*android:animator/fragment_fade_enter</item>
+        <item name="android:fragmentFadeExitAnimation">@*android:animator/fragment_fade_exit</item>
+    </style>
 </resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/styles_device_default.xml b/car_product/car_ui_portrait/rro/android/res/values/styles_device_default.xml
index de80014..fadcfd5 100644
--- a/car_product/car_ui_portrait/rro/android/res/values/styles_device_default.xml
+++ b/car_product/car_ui_portrait/rro/android/res/values/styles_device_default.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -119,6 +119,7 @@
     <style name="Widget.DeviceDefault.CompoundButton.Switch" parent="android:Widget.Material.CompoundButton.Switch">
         <item name="android:thumb">@*android:drawable/car_switch_thumb</item>
         <item name="android:track">@*android:drawable/car_switch_track</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
     <style name="Widget.DeviceDefault.ProgressBar.Horizontal" parent="android:Widget.Material.ProgressBar.Horizontal">
diff --git a/car_product/car_ui_portrait/rro/android/res/values/themes_device_defaults.xml b/car_product/car_ui_portrait/rro/android/res/values/themes_device_defaults.xml
index 7075424..dc0198b 100644
--- a/car_product/car_ui_portrait/rro/android/res/values/themes_device_defaults.xml
+++ b/car_product/car_ui_portrait/rro/android/res/values/themes_device_defaults.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -42,19 +42,11 @@
 
         <!-- Color palette -->
         <item name="android:statusBarColor">@android:color/black</item>
-        <item name="android:textColorHint">@*android:color/car_body2</item>
-        <item name="android:textColorPrimary">@*android:color/text_color_primary</item>
-        <item name="android:textColorSecondary">@*android:color/car_body2</item>
-        <item name="android:colorAccent">@*android:color/accent_device_default_light</item>
-        <item name="android:colorBackground">@*android:color/background_device_default_light</item>
-        <item name="android:colorBackgroundFloating">@*android:color/background_floating_device_default_light</item>
-        <item name="android:colorButtonNormal">@*android:color/car_highlight</item>
-        <item name="android:colorControlHighlight">@*android:color/car_card_ripple_background</item>
-        <item name="android:colorControlNormal">@*android:color/car_body2</item>
-        <item name="android:colorPrimary">@*android:color/primary_device_default_light</item>
-        <item name="android:colorPrimaryDark">@*android:color/primary_device_default_dark</item>
-        <item name="android:colorForeground">@*android:color/car_card_light</item>
-        <item name="android:editTextColor">@*android:color/car_body1</item>
+        <item name="android:colorButtonNormal">@color/btn_device_default_dark</item>
+        <item name="android:colorControlHighlight">@color/btn_device_default_dark</item>
+        <item name="android:colorControlNormal">@color/btn_device_default_dark</item>
+
+        <item name="android:listDivider">@color/list_divider_color</item>
     </style>
 
     <style name="Theme.DeviceDefault.Dialog" parent="android:Theme.Material.Dialog">
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_primary.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_primary.xml
index 860f219..e87a692 100644
--- a/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_primary.xml
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_primary.xml
@@ -1,28 +1,28 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    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.
--->
+  ~ 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.
+  -->
 <!-- Copy of ?android:attr/textColorPrimary (frameworks/base/res/res/color/text_color_primary.xml)
      but with a ux restricted state. -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:app="http://schemas.android.com/apk/res-auto">
     <item android:state_enabled="false"
           android:alpha="?android:attr/disabledAlpha"
-          android:color="?android:attr/colorForeground"/>
+          android:color="?android:attr/textColorPrimary"/>
     <item app:state_ux_restricted="true"
           android:alpha="?android:attr/disabledAlpha"
-          android:color="?android:attr/colorForeground"/>
-    <item android:color="?android:attr/colorForeground"/>
+          android:color="?android:attr/textColorPrimary"/>
+    <item android:color="?android:attr/textColorPrimary"/>
 </selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_secondary.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_secondary.xml
index f99fc86..0f1fcb5 100644
--- a/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_secondary.xml
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_secondary.xml
@@ -1,28 +1,28 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    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.
--->
+  ~ 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.
+  -->
 <!-- Copy of ?android:attr/textColorSecondary (frameworks/base/res/res/color/text_color_secondary.xml)
      but with a ux restricted state. -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:app="http://schemas.android.com/apk/res-auto">
     <item android:state_enabled="false"
           android:alpha="?android:attr/disabledAlpha"
-          android:color="?android:attr/colorForeground"/>
+          android:color="?android:attr/textColorSecondary"/>
     <item app:state_ux_restricted="true"
           android:alpha="?android:attr/disabledAlpha"
-          android:color="?android:attr/colorForeground"/>
-    <item android:color="?android:attr/colorForeground"/>
+          android:color="?android:attr/textColorSecondary"/>
+    <item android:color="?android:attr/textColorSecondary"/>
 </selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_activity_background.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_activity_background.xml
new file mode 100644
index 0000000..f70ad67
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_activity_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item android:drawable="?android:attr/colorBackground"/>
+</layer-list>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/drawables.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/drawables.xml
index 7bf9ad7..f44dbf0 100644
--- a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/drawables.xml
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/drawables.xml
@@ -1,19 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  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.
+  ~ 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.
   -->
-<resources>
-    <drawable name="car_ui_activity_background">#FDFDFD</drawable>
-</resources>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Toolbar background color -->
+    <drawable name="car_ui_toolbar_background">@*android:color/background_device_default_dark</drawable>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/styles.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/styles.xml
index cf229bd..f154c0b 100644
--- a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/styles.xml
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/styles.xml
@@ -17,9 +17,36 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
     <style name="TextAppearance_CarUi_AlertDialog_Title" parent="android:TextAppearance.DeviceDefault">
-        <item name="android:textSize">@dimen/car_ui_body1_size</item>
+        <item name="android:textSize">32sp</item>
     </style>
     <style name="TextAppearance_CarUi_AlertDialog_Subtitle" parent="android:TextAppearance.DeviceDefault">
-        <item name="android:textSize">@dimen/car_ui_body3_size</item>
+        <item name="android:textSize">24sp</item>
     </style>
-</resources>
\ No newline at end of file
+
+    <style name="TextAppearance.CarUi.PreferenceCategoryTitle" parent="android:TextAppearance.DeviceDefault">
+        <item name="android:fontFamily">sans-serif-medium</item>
+        <item name="android:textColor">?android:attr/colorAccent</item>
+        <item name="android:textSize">24sp</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.PreferenceSummary" parent="android:TextAppearance.DeviceDefault">
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textSize">24sp</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.PreferenceTitle" parent="android:TextAppearance.DeviceDefault">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textSize">32sp</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.Widget" parent="android:TextAppearance.DeviceDefault.Widget">
+        <item name="android:textAlignment">viewStart</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.Widget.Toolbar"/>
+
+    <style name="TextAppearance.CarUi.Widget.Toolbar.Title">
+        <item name="android:singleLine">true</item>
+        <item name="android:textSize">32sp</item>
+    </style>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/themes.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/themes.xml
index 62b5c1b..85dfc95 100644
--- a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/themes.xml
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/themes.xml
@@ -14,14 +14,5 @@
   ~ limitations under the License.
   -->
 <resources>
-    <style name="TextAppearance.CarUi.Widget" parent="android:TextAppearance.Material.Widget">
-        <item name="android:textAlignment">viewStart</item>
-    </style>
 
-    <style name="TextAppearance.CarUi.Widget.Toolbar"/>
-
-    <style name="TextAppearance.CarUi.Widget.Toolbar.Title">
-    <item name="android:singleLine">true</item>
-    <item name="android:textSize">32sp</item>
-    </style>
 </resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/values.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/values.xml
new file mode 100644
index 0000000..a09f6e8
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<resources>
+    <bool name="car_ui_scrollbar_enable">false</bool>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/xml/overlays.xml
index 0099f9b..17c973e 100644
--- a/car_product/car_ui_portrait/rro/car-ui-customizations/res/xml/overlays.xml
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/xml/overlays.xml
@@ -23,6 +23,7 @@
 
     <item target="dimen/car_ui_body1_size" value="@dimen/car_ui_body1_size"/>
     <item target="dimen/car_ui_body3_size" value="@dimen/car_ui_body3_size"/>
+    <item target="dimen/alert_dialog_margin" value="@dimen/alert_dialog_margin"/>
 
     <item target="drawable/car_ui_recyclerview_ic_up" value="@drawable/car_ui_recyclerview_ic_up" />
     <item target="drawable/car_ui_recyclerview_ic_down" value="@drawable/car_ui_recyclerview_ic_down" />
@@ -30,5 +31,24 @@
     <item target="drawable/car_ui_activity_background" value="@drawable/car_ui_activity_background" />
     <item target="drawable/car_ui_toolbar_menu_item_icon_background" value="@drawable/car_ui_toolbar_menu_item_icon_background" />
 
+    <item target="color/car_ui_text_color_primary" value="@color/car_ui_text_color_primary" />
+    <item target="color/car_ui_text_color_secondary" value="@color/car_ui_text_color_secondary" />
+    <item target="color/car_ui_toolbar_tab_item_selector" value="@color/car_ui_toolbar_tab_item_selector" />
+
+
+    <item target="drawable/car_ui_toolbar_background" value="@drawable/car_ui_toolbar_background" />
+    <item target="drawable/car_ui_toolbar_menu_item_divider" value="@drawable/car_ui_toolbar_menu_item_divider" />
+    <item target="drawable/car_ui_toolbar_menu_item_icon_ripple" value="@drawable/car_ui_toolbar_menu_item_icon_ripple" />
+    <item target="bool/car_ui_scrollbar_enable" value="@bool/car_ui_scrollbar_enable" />
+
+    <item target="style/TextAppearance_CarUi_AlertDialog_Title" value="@style/TextAppearance_CarUi_AlertDialog_Title" />
+    <item target="style/TextAppearance_CarUi_AlertDialog_Subtitle" value="@style/TextAppearance_CarUi_AlertDialog_Subtitle" />
+    <item target="style/TextAppearance.CarUi.PreferenceCategoryTitle" value="@style/TextAppearance.CarUi.PreferenceCategoryTitle" />
+    <item target="style/TextAppearance.CarUi.PreferenceSummary" value="@style/TextAppearance.CarUi.PreferenceSummary" />
+    <item target="style/TextAppearance.CarUi.PreferenceTitle" value="@style/TextAppearance.CarUi.PreferenceTitle" />
+    <item target="style/TextAppearance.CarUi.Widget" value="@style/TextAppearance.CarUi.Widget" />
+    <item target="style/TextAppearance.CarUi.Widget.Toolbar" value="@style/TextAppearance.CarUi.Widget.Toolbar" />
+    <item target="style/TextAppearance.CarUi.Widget.Toolbar.Title" value="@style/TextAppearance.CarUi.Widget.Toolbar.Title" />
+
     <item target="attr/state_ux_restricted" value="@attr/state_ux_restricted"/>
 </overlay>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_base_layout_toolbar.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_base_layout_toolbar.xml
index 5aa301b..39d910c 100644
--- a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_base_layout_toolbar.xml
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_base_layout_toolbar.xml
@@ -64,7 +64,6 @@
                 android:id="@+id/car_ui_toolbar_nav_icon_container"
                 android:layout_width="90dp"
                 android:layout_height="0dp"
-                android:layout_marginStart="88dp"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toTopOf="parent">
@@ -163,7 +162,7 @@
             <View
                 android:layout_width="match_parent"
                 android:layout_height="2dp"
-                android:background="#F1F3F4"
+                android:background="?android:attr/listDivider"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent" />
@@ -176,6 +175,7 @@
         android:layout_width="wrap_content"
         android:layout_height="0dp"
         android:tag="car_ui_left_inset"
+        android:orientation="horizontal"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/top_part_of_toolbar_focus_area"
         app:layout_constraintBottom_toBottomOf="parent">
@@ -190,10 +190,11 @@
     <!-- Hairline to the right of the tabs -->
     <View
         android:layout_width="2dp"
-        android:layout_height="0dp"
-        android:background="#F1F3F4"
-        app:layout_constraintBottom_toBottomOf="@id/left_part_of_toolbar_focus_area"
-        app:layout_constraintTop_toTopOf="@id/left_part_of_toolbar_focus_area"
-        app:layout_constraintEnd_toEndOf="@id/left_part_of_toolbar_focus_area" />
+        android:layout_height="match_parent"
+        android:background="?android:attr/listDivider"
+        android:focusable="false"
+        app:layout_constraintStart_toEndOf="@id/left_part_of_toolbar_focus_area"
+        app:layout_constraintTop_toBottomOf="@id/top_part_of_toolbar_focus_area"
+        app:layout_constraintBottom_toBottomOf="parent"/>
 
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car_product/car_ui_portrait/rro/car_ui_portrait_rro.mk b/car_product/car_ui_portrait/rro/car_ui_portrait_rro.mk
index 5029fcd..5a4082b 100644
--- a/car_product/car_ui_portrait/rro/car_ui_portrait_rro.mk
+++ b/car_product/car_ui_portrait/rro/car_ui_portrait_rro.mk
@@ -19,12 +19,13 @@
 
 # All RROs to be included in car_ui_portrait builds.
 PRODUCT_PACKAGES += \
+    CarEvsCameraPreviewAppRRO \
     CarUiPortraitDialerRRO \
     CarUiPortraitMediaRRO \
     CarUiPortraitLauncherRRO \
     CarUiPortraitNotificationRRO \
     CarUiPortraitFrameworkResRRO \
-    CarUiPortraitFrameworkResRROTest \
+    CarUiPortraitFrameworkResRROTest
 
 ifneq ($(INCLUDE_SEAHAWK_ONLY_RROS),)
 PRODUCT_PACKAGES += \
diff --git a/car_product/init/init.car.rc b/car_product/init/init.car.rc
index a4880ab..4780dd4 100644
--- a/car_product/init/init.car.rc
+++ b/car_product/init/init.car.rc
@@ -1,6 +1,7 @@
 # Insert car-specific startup services here
 on post-fs-data
     mkdir /data/system/car 0700 system system
+    mkdir /data/system/car/watchdog 0700 system system
 
 # A property to enable EVS services conditionally
 on property:persist.automotive.evs.mode=0
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-da/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-da/strings.xml
index 1c751f9..6db644f 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-da/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-da/strings.xml
@@ -18,6 +18,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Chauffør"</string>
-    <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"få kun adgang til omtrentlig placering i forgrunden"</string>
+    <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"få kun adgang til omtrentlig lokation i forgrunden"</string>
     <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Aktivér mikrofon"</string>
 </resources>
diff --git a/cpp/evs/manager/1.1/VirtualCamera.cpp b/cpp/evs/manager/1.1/VirtualCamera.cpp
index a7e6329..3b0ef95 100644
--- a/cpp/evs/manager/1.1/VirtualCamera.cpp
+++ b/cpp/evs/manager/1.1/VirtualCamera.cpp
@@ -399,6 +399,9 @@
                             if (pHwCamera == nullptr) {
                                 continue;
                             }
+                            if (mFramesHeld[key].size() == 0) {
+                                continue;
+                            }
 
                             const auto frame = mFramesHeld[key].back();
                             if (frame.timestamp > lastFrameTimestamp) {
diff --git a/cpp/security/vehicle_binding_util/Android.bp b/cpp/security/vehicle_binding_util/Android.bp
index 9867011..3f235b1 100644
--- a/cpp/security/vehicle_binding_util/Android.bp
+++ b/cpp/security/vehicle_binding_util/Android.bp
@@ -28,7 +28,6 @@
         "android.hardware.automotive.vehicle@2.0",
         "libbase",
         "libbinder",
-        "libcrypto",
         "libcutils",
         "libhidlbase",
         "liblog",
diff --git a/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp
index 60e6376..c15bd3c 100644
--- a/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp
+++ b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp
@@ -20,9 +20,6 @@
 #include <android/hardware/automotive/vehicle/2.0/types.h>
 #include <cutils/properties.h>  // for property_get
 #include <logwrap/logwrap.h>
-#include <openssl/digest.h>
-#include <openssl/hmac.h>
-#include <openssl/sha.h>
 #include <utils/SystemClock.h>
 
 #include <fcntl.h>
@@ -74,19 +71,6 @@
     return out;
 }
 
-std::string getDigestString(const std::vector<uint8_t>& bytes) {
-    // Use HMAC so the digest isn't useful for anything else
-    constexpr char kKey[] = "vehicle binding util seed digest";
-    std::vector<uint8_t> mac(SHA_DIGEST_LENGTH);
-    if (nullptr ==
-        HMAC(EVP_sha1(), kKey, sizeof(kKey), bytes.data(), bytes.size(), mac.data(),
-             nullptr /* out_len */)) {
-        return "<error>";
-    }
-
-    return toHexString(mac);
-}
-
 BindingStatus setSeedVhalProperty(sp<IVehicle> vehicle, const std::vector<uint8_t>& seed) {
     VehiclePropValue propValue;
     propValue.timestamp = elapsedRealtimeNano();
@@ -203,8 +187,7 @@
 
     status = sendSeedToVold(executor, seed);
     if (status == BindingStatus::OK) {
-        LOG(INFO) << "Successfully bound vehicle storage to seed with digest "
-                  << getDigestString(seed);
+        LOG(INFO) << "Successfully bound vehicle storage to seed.";
     }
     return status;
 }
diff --git a/packages/ScriptExecutor/Android.bp b/packages/ScriptExecutor/Android.bp
index 6e35f4e..af0d132 100644
--- a/packages/ScriptExecutor/Android.bp
+++ b/packages/ScriptExecutor/Android.bp
@@ -11,9 +11,10 @@
 // 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
 
 cc_defaults {
     name: "scriptexecutor_defaults",
@@ -22,9 +23,12 @@
         "-Wno-unused-parameter",
     ],
 
-    static_libs: [
+    shared_libs: [
         "libbase",
         "liblog",
+    ],
+
+    static_libs: [
         "liblua",
     ],
 }
@@ -45,7 +49,6 @@
     ],
 
     shared_libs: [
-        "libandroid_runtime",
         "libbinder",
         "libnativehelper",
         "libutils",
@@ -75,15 +78,16 @@
 android_app {
     name: "ScriptExecutor",
 
-    srcs: ["src/**/*.java"],
+    srcs: [
+        ":iscriptexecutor_aidl",
+        "src/**/*.java"
+    ],
 
     resource_dirs: ["res"],
 
+    // TODO(197006437): Make this build against sdk_version: "module_current" instead.
     platform_apis: true,
 
-    // Each update should be signed by OEMs
-    certificate: "platform",
-
     privileged: false,
 
     // TODO(b/196053524): Enable optimization.
@@ -91,47 +95,32 @@
         enabled: false,
     },
 
-    static_libs: [
-        "com.android.car.scriptexecutor-interface-lib",
-    ],
+    aidl: {
+        include_dirs: [
+            // TODO(b/198195711): Remove once we compile against SDK.
+	    "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+	],
+    },
 
     jni_libs: [
         "libscriptexecutorjni",
-    ],
-
-}
-
-
-android_library {
-    name: "com.android.car.scriptexecutor-interface-lib",
-
-    srcs: [
-        "interface/**/*.aidl",
-    ],
-
-    sdk_version: "system_current",
-
-    aidl: {
-        local_include_dirs: [
-            "interface",
-        ],
-    },
+    ]
 }
 
 java_test_helper_library {
     name: "scriptexecutor-test-lib",
 
     srcs: [
+        ":iscriptexecutor_aidl",
+        ":iscriptexecutorconstants_aidl",
         "src/**/*.java",
     ],
 
-    static_libs: ["com.android.car.scriptexecutor-interface-lib"],
+    aidl: {
+        include_dirs: [
+            // TODO(b/198195711): Remove once we compile against SDK.
+	    "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+	],
+    },
 }
 
-filegroup {
-    name: "iscriptexecutorconstants_aidl",
-
-    srcs: ["interface/com/android/car/scriptexecutor/IScriptExecutorConstants.aidl"],
-
-    path: "interface",
-}
diff --git a/packages/ScriptExecutor/AndroidManifest.xml b/packages/ScriptExecutor/AndroidManifest.xml
index 9a59ba2..51595fa 100644
--- a/packages/ScriptExecutor/AndroidManifest.xml
+++ b/packages/ScriptExecutor/AndroidManifest.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- 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.
diff --git a/packages/ScriptExecutor/res/values/strings.xml b/packages/ScriptExecutor/res/values/strings.xml
index bb219dd..39ca8b2 100644
--- a/packages/ScriptExecutor/res/values/strings.xml
+++ b/packages/ScriptExecutor/res/values/strings.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- 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.
@@ -13,6 +13,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+<resources>
     <string name="app_title" translatable="false">Script executor service</string>
 </resources>
diff --git a/packages/ScriptExecutor/src/BundleWrapper.cpp b/packages/ScriptExecutor/src/BundleWrapper.cpp
index 28412cc..d447d34 100644
--- a/packages/ScriptExecutor/src/BundleWrapper.cpp
+++ b/packages/ScriptExecutor/src/BundleWrapper.cpp
@@ -17,7 +17,6 @@
 #include "BundleWrapper.h"
 
 #include <android-base/logging.h>
-#include <android_runtime/AndroidRuntime.h>
 
 namespace com {
 namespace android {
@@ -26,8 +25,8 @@
 
 BundleWrapper::BundleWrapper(JNIEnv* env) {
     mJNIEnv = env;
-    mBundleClass =
-            static_cast<jclass>(mJNIEnv->NewGlobalRef(mJNIEnv->FindClass("android/os/Bundle")));
+    mBundleClass = static_cast<jclass>(
+            mJNIEnv->NewGlobalRef(mJNIEnv->FindClass("android/os/PersistableBundle")));
     jmethodID bundleConstructor = mJNIEnv->GetMethodID(mBundleClass, "<init>", "()V");
     mBundle = mJNIEnv->NewGlobalRef(mJNIEnv->NewObject(mBundleClass, bundleConstructor));
 }
@@ -43,6 +42,7 @@
 }
 
 void BundleWrapper::putBoolean(const char* key, bool value) {
+    // TODO(b/188832769): consider caching the references.
     jmethodID putBooleanMethod =
             mJNIEnv->GetMethodID(mBundleClass, "putBoolean", "(Ljava/lang/String;Z)V");
     mJNIEnv->CallVoidMethod(mBundle, putBooleanMethod, mJNIEnv->NewStringUTF(key),
diff --git a/packages/ScriptExecutor/src/BundleWrapper.h b/packages/ScriptExecutor/src/BundleWrapper.h
index aff63bc..4a0f9bb 100644
--- a/packages/ScriptExecutor/src/BundleWrapper.h
+++ b/packages/ScriptExecutor/src/BundleWrapper.h
@@ -34,7 +34,8 @@
 
     virtual ~BundleWrapper();
 
-    // Family of methods that puts the provided 'value' into the Bundle under provided 'key'.
+    // Family of methods that puts the provided 'value' into the PersistableBundle
+    // under provided 'key'.
     void putBoolean(const char* key, bool value);
     void putInteger(const char* key, int value);
     void putDouble(const char* key, double value);
@@ -43,12 +44,12 @@
     jobject getBundle();
 
 private:
-    // The class asks Java to create Bundle object and stores the reference.
-    // When the instance of this class is destroyed the actual Java Bundle object behind
+    // The class asks Java to create PersistableBundle object and stores the reference.
+    // When the instance of this class is destroyed the actual Java PersistableBundle object behind
     // this reference stays on and is managed by Java.
     jobject mBundle;
 
-    // Reference to java Bundle class cached for performance reasons.
+    // Reference to java PersistableBundle class cached for performance reasons.
     jclass mBundleClass;
 
     // Stores a JNIEnv* pointer.
diff --git a/packages/ScriptExecutor/src/JniUtils.cpp b/packages/ScriptExecutor/src/JniUtils.cpp
index df3c899..e943129 100644
--- a/packages/ScriptExecutor/src/JniUtils.cpp
+++ b/packages/ScriptExecutor/src/JniUtils.cpp
@@ -30,8 +30,9 @@
 
     // TODO(b/188832769): Consider caching some of these JNI references for
     // performance reasons.
-    jclass bundleClass = env->FindClass("android/os/Bundle");
-    jmethodID getKeySetMethod = env->GetMethodID(bundleClass, "keySet", "()Ljava/util/Set;");
+    jclass persistableBundleClass = env->FindClass("android/os/PersistableBundle");
+    jmethodID getKeySetMethod =
+            env->GetMethodID(persistableBundleClass, "keySet", "()Ljava/util/Set;");
     jobject keys = env->CallObjectMethod(bundle, getKeySetMethod);
     jclass setClass = env->FindClass("java/util/Set");
     jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
@@ -48,8 +49,8 @@
     // TODO(b/188816922): Handle more types such as float and integer arrays,
     // and perhaps nested Bundles.
 
-    jmethodID getMethod =
-            env->GetMethodID(bundleClass, "get", "(Ljava/lang/String;)Ljava/lang/Object;");
+    jmethodID getMethod = env->GetMethodID(persistableBundleClass, "get",
+                                           "(Ljava/lang/String;)Ljava/lang/Object;");
 
     // Iterate over key set of the bundle one key at a time.
     while (env->CallBooleanMethod(keySetIteratorObject, hasNextMethod)) {
diff --git a/packages/ScriptExecutor/src/LuaEngine.cpp b/packages/ScriptExecutor/src/LuaEngine.cpp
index 4b86c2e..cf418bf 100644
--- a/packages/ScriptExecutor/src/LuaEngine.cpp
+++ b/packages/ScriptExecutor/src/LuaEngine.cpp
@@ -19,7 +19,7 @@
 #include "BundleWrapper.h"
 
 #include <android-base/logging.h>
-#include <com/android/car/scriptexecutor/IScriptExecutorConstants.h>
+#include <com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.h>
 
 #include <utility>
 
@@ -34,12 +34,48 @@
 namespace car {
 namespace scriptexecutor {
 
+using ::com::android::car::telemetry::scriptexecutorinterface::IScriptExecutorConstants;
+
 namespace {
 
 enum LuaNumReturnedResults {
     ZERO_RETURNED_RESULTS = 0,
 };
 
+// Helper method that goes over Lua table fields one by one and populates PersistableBundle
+// object wrapped in BundleWrapper.
+// It is assumed that Lua table is located on top of the Lua stack.
+void convertLuaTableToBundle(lua_State* lua, BundleWrapper* bundleWrapper) {
+    // Iterate over Lua table which is expected to be at the top of Lua stack.
+    // lua_next call pops the key from the top of the stack and finds the next
+    // key-value pair. It returns 0 if the next pair was not found.
+    // More on lua_next in: https://www.lua.org/manual/5.3/manual.html#lua_next
+    lua_pushnil(lua);  // First key is a null value.
+    while (lua_next(lua, /* index = */ -2) != 0) {
+        //  'key' is at index -2 and 'value' is at index -1
+        // -1 index is the top of the stack.
+        // remove 'value' and keep 'key' for next iteration
+        // Process each key-value depending on a type and push it to Java PersistableBundle.
+        const char* key = lua_tostring(lua, /* index = */ -2);
+        if (lua_isboolean(lua, /* index = */ -1)) {
+            bundleWrapper->putBoolean(key, static_cast<bool>(lua_toboolean(lua, /* index = */ -1)));
+        } else if (lua_isinteger(lua, /* index = */ -1)) {
+            bundleWrapper->putInteger(key, static_cast<int>(lua_tointeger(lua, /* index = */ -1)));
+        } else if (lua_isnumber(lua, /* index = */ -1)) {
+            bundleWrapper->putDouble(key, static_cast<double>(lua_tonumber(lua, /* index = */ -1)));
+        } else if (lua_isstring(lua, /* index = */ -1)) {
+            bundleWrapper->putString(key, lua_tostring(lua, /* index = */ -1));
+        } else {
+            // not supported yet...
+            LOG(WARNING) << "key=" << key << " has a Lua type which is not supported yet. "
+                         << "The bundle object will not have this key-value pair.";
+        }
+        // Pop value from the stack, keep the key for the next iteration.
+        lua_pop(lua, 1);
+        // The key is at index -1, the table is at index -2 now.
+    }
+}
+
 }  // namespace
 
 ScriptExecutorListener* LuaEngine::sListener = nullptr;
@@ -85,6 +121,7 @@
 
     // Register limited set of reserved methods for Lua to call native side.
     lua_register(mLuaState, "on_success", LuaEngine::onSuccess);
+    lua_register(mLuaState, "on_script_finished", LuaEngine::onScriptFinished);
     lua_register(mLuaState, "on_error", LuaEngine::onError);
     return status;
 }
@@ -116,40 +153,15 @@
 int LuaEngine::onSuccess(lua_State* lua) {
     // Any script we run can call on_success only with a single argument of Lua table type.
     if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
-        // TODO(b/193565932): Return programming error through binder callback interface.
-        LOG(ERROR) << "Only a single input argument, a Lua table object, expected here";
+        sListener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
+                           "on_success can push only a single parameter from Lua - a Lua table",
+                           "");
+        return ZERO_RETURNED_RESULTS;
     }
 
-    // Helper object to create and populate Java Bundle object.
+    // Helper object to create and populate Java PersistableBundle object.
     BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
-    // Iterate over Lua table which is expected to be at the top of Lua stack.
-    // lua_next call pops the key from the top of the stack and finds the next
-    // key-value pair for the popped key. It returns 0 if the next pair was not found.
-    // More on lua_next in: https://www.lua.org/manual/5.3/manual.html#lua_next
-    lua_pushnil(lua);  // First key is a null value.
-    while (lua_next(lua, /* index = */ -2) != 0) {
-        //  'key' is at index -2 and 'value' is at index -1
-        // -1 index is the top of the stack.
-        // remove 'value' and keep 'key' for next iteration
-        // Process each key-value depending on a type and push it to Java Bundle.
-        const char* key = lua_tostring(lua, /* index = */ -2);
-        if (lua_isboolean(lua, /* index = */ -1)) {
-            bundleWrapper.putBoolean(key, static_cast<bool>(lua_toboolean(lua, /* index = */ -1)));
-        } else if (lua_isinteger(lua, /* index = */ -1)) {
-            bundleWrapper.putInteger(key, static_cast<int>(lua_tointeger(lua, /* index = */ -1)));
-        } else if (lua_isnumber(lua, /* index = */ -1)) {
-            bundleWrapper.putDouble(key, static_cast<double>(lua_tonumber(lua, /* index = */ -1)));
-        } else if (lua_isstring(lua, /* index = */ -1)) {
-            bundleWrapper.putString(key, lua_tostring(lua, /* index = */ -1));
-        } else {
-            // not supported yet...
-            LOG(WARNING) << "key=" << key << " has a Lua type which is not supported yet. "
-                         << "The bundle object will not have this key-value pair.";
-        }
-        // Pop 1 element from the stack.
-        lua_pop(lua, 1);
-        // The key is at index -1, the table is at index -2 now.
-    }
+    convertLuaTableToBundle(lua, &bundleWrapper);
 
     // Forward the populated Bundle object to Java callback.
     sListener->onSuccess(bundleWrapper.getBundle());
@@ -158,6 +170,27 @@
     return ZERO_RETURNED_RESULTS;
 }
 
+int LuaEngine::onScriptFinished(lua_State* lua) {
+    // Any script we run can call on_success only with a single argument of Lua table type.
+    if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
+        sListener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
+                           "on_script_finished can push only a single parameter from Lua - a Lua "
+                           "table",
+                           "");
+        return ZERO_RETURNED_RESULTS;
+    }
+
+    // Helper object to create and populate Java PersistableBundle object.
+    BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
+    convertLuaTableToBundle(lua, &bundleWrapper);
+
+    // Forward the populated Bundle object to Java callback.
+    sListener->onScriptFinished(bundleWrapper.getBundle());
+    // We explicitly must tell Lua how many results we return, which is 0 in this case.
+    // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+    return ZERO_RETURNED_RESULTS;
+}
+
 int LuaEngine::onError(lua_State* lua) {
     // Any script we run can call on_error only with a single argument of Lua string type.
     if (lua_gettop(lua) != 1 || !lua_isstring(lua, /* index = */ -1)) {
diff --git a/packages/ScriptExecutor/src/LuaEngine.h b/packages/ScriptExecutor/src/LuaEngine.h
index 20c7ae5..0774108 100644
--- a/packages/ScriptExecutor/src/LuaEngine.h
+++ b/packages/ScriptExecutor/src/LuaEngine.h
@@ -63,13 +63,25 @@
     // The script will provide the results as a Lua table.
     // We currently support only non-nested fields in the table and the fields can be the following
     // Lua types: boolean, number, integer, and string.
-    // The result pushed by Lua is converted to Android Bundle and forwarded to
+    // The result pushed by Lua is converted to PersistableBundle and forwarded to
     // ScriptExecutor service via callback interface.
     // This method returns 0 to indicate that no results were pushed to Lua stack according
     // to Lua C function calling convention.
     // More info: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
     static int onSuccess(lua_State* lua);
 
+    // Invoked by a running Lua script to effectively mark the completion of the script's lifecycle,
+    // and send the final results to CarTelemetryService and then to the user.
+    // The script will provide the final results as a Lua table.
+    // We currently support only non-nested fields in the table and the fields can be the following
+    // Lua types: boolean, number, integer, and string.
+    // The result pushed by Lua is converted to Android PersistableBundle and forwarded to
+    // ScriptExecutor service via callback interface.
+    // This method returns 0 to indicate that no results were pushed to Lua stack according
+    // to Lua C function calling convention.
+    // More info: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+    static int onScriptFinished(lua_State* lua);
+
     // Invoked by a running Lua script to indicate than an error occurred. This is the mechanism to
     // for a script author to receive error logs. The caller script encapsulates all the information
     // about the error that the author wants to provide in a single string parameter.
diff --git a/packages/ScriptExecutor/src/ScriptExecutorListener.cpp b/packages/ScriptExecutor/src/ScriptExecutorListener.cpp
index 1a70259..739c71d 100644
--- a/packages/ScriptExecutor/src/ScriptExecutorListener.cpp
+++ b/packages/ScriptExecutor/src/ScriptExecutorListener.cpp
@@ -37,23 +37,23 @@
 
 void ScriptExecutorListener::onSuccess(jobject bundle) {
     JNIEnv* env = getCurrentJNIEnv();
-    if (mScriptExecutorListener == nullptr) {
-        env->FatalError(
-                "mScriptExecutorListener must point to a valid listener object, not nullptr.");
-    }
     jclass listenerClass = env->GetObjectClass(mScriptExecutorListener);
     jmethodID onSuccessMethod =
-            env->GetMethodID(listenerClass, "onSuccess", "(Landroid/os/Bundle;)V");
+            env->GetMethodID(listenerClass, "onSuccess", "(Landroid/os/PersistableBundle;)V");
     env->CallVoidMethod(mScriptExecutorListener, onSuccessMethod, bundle);
 }
 
+void ScriptExecutorListener::onScriptFinished(jobject bundle) {
+    JNIEnv* env = getCurrentJNIEnv();
+    jclass listenerClass = env->GetObjectClass(mScriptExecutorListener);
+    jmethodID onScriptFinished = env->GetMethodID(listenerClass, "onScriptFinished",
+                                                  "(Landroid/os/PersistableBundle;)V");
+    env->CallVoidMethod(mScriptExecutorListener, onScriptFinished, bundle);
+}
+
 void ScriptExecutorListener::onError(const int errorType, const char* message,
                                      const char* stackTrace) {
     JNIEnv* env = getCurrentJNIEnv();
-    if (mScriptExecutorListener == nullptr) {
-        env->FatalError(
-                "mScriptExecutorListener must point to a valid listener object, not nullptr.");
-    }
     jclass listenerClass = env->GetObjectClass(mScriptExecutorListener);
     jmethodID onErrorMethod =
             env->GetMethodID(listenerClass, "onError", "(ILjava/lang/String;Ljava/lang/String;)V");
diff --git a/packages/ScriptExecutor/src/ScriptExecutorListener.h b/packages/ScriptExecutor/src/ScriptExecutorListener.h
index f58ba0d..392bc77 100644
--- a/packages/ScriptExecutor/src/ScriptExecutorListener.h
+++ b/packages/ScriptExecutor/src/ScriptExecutorListener.h
@@ -33,7 +33,7 @@
 
     virtual ~ScriptExecutorListener();
 
-    void onScriptFinished() {}
+    void onScriptFinished(jobject bundle);
 
     void onSuccess(jobject bundle);
 
diff --git a/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java b/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java
index 462196e..bd364bc 100644
--- a/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java
+++ b/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java
@@ -18,10 +18,13 @@
 
 import android.app.Service;
 import android.content.Intent;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
 
 /**
  * Executes Lua code in an isolated process with provided source code
@@ -42,8 +45,9 @@
 
     private final class IScriptExecutorImpl extends IScriptExecutor.Stub {
         @Override
-        public void invokeScript(String scriptBody, String functionName, Bundle publishedData,
-                Bundle savedState, IScriptExecutorListener listener) {
+        public void invokeScript(String scriptBody, String functionName,
+                PersistableBundle publishedData, PersistableBundle savedState,
+                IScriptExecutorListener listener) {
             mNativeHandler.post(() ->
                     nativeInvokeScript(mLuaEnginePtr, scriptBody, functionName, publishedData,
                             savedState, listener));
@@ -105,6 +109,6 @@
      *                      and errors.
      */
     private native void nativeInvokeScript(long luaEnginePtr, String scriptBody,
-            String functionName, Bundle publishedData, Bundle savedState,
+            String functionName, PersistableBundle publishedData, PersistableBundle savedState,
             IScriptExecutorListener listener);
 }
diff --git a/packages/ScriptExecutor/tests/unit/Android.bp b/packages/ScriptExecutor/tests/unit/Android.bp
index 07efd38..fbbe750 100644
--- a/packages/ScriptExecutor/tests/unit/Android.bp
+++ b/packages/ScriptExecutor/tests/unit/Android.bp
@@ -27,10 +27,6 @@
 
     certificate: "platform",
 
-    optimize: {
-        enabled: true,
-    },
-
     instrumentation_for: "ScriptExecutor",
 
     static_libs: [
@@ -48,3 +44,20 @@
         "libscriptexecutorjniutils-test",
     ],
 }
+
+cc_library_shared {
+    name: "libscriptexecutorjniutils-test",
+
+    defaults: [
+        "scriptexecutor_defaults",
+    ],
+
+    srcs: [
+        "src/com/android/car/scriptexecutor/JniUtilsTestHelper.cpp",
+    ],
+
+    shared_libs: [
+        "libnativehelper",
+        "libscriptexecutor",
+    ],
+}
diff --git a/packages/ScriptExecutor/tests/unit/AndroidManifest.xml b/packages/ScriptExecutor/tests/unit/AndroidManifest.xml
index cd6ac04..f393408 100644
--- a/packages/ScriptExecutor/tests/unit/AndroidManifest.xml
+++ b/packages/ScriptExecutor/tests/unit/AndroidManifest.xml
@@ -16,13 +16,19 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.car.scriptexecutor_unittest">
+          package="com.android.car.scriptexecutor_test">
+
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+    <queries>
+        <package android:name="com.android.car.scriptexecutor" />
+    </queries>
 
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.car.scriptexecutor_unittest"
-                     android:label="Unit Tests for ScriptExecutor"/>
+                     android:targetPackage="com.android.car.scriptexecutor_test"
+                     android:label="Tests for ScriptExecutor"/>
 </manifest>
diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTest.java b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTest.java
index 13379cc..a1a5979 100644
--- a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTest.java
+++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTest.java
@@ -18,7 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.util.Log;
 
 import org.junit.After;
@@ -60,7 +60,8 @@
     }
 
     // Simply invokes PushBundleToLuaTable native method under test.
-    private native void nativePushBundleToLuaTableCaller(long luaEnginePtr, Bundle bundle);
+    private native void nativePushBundleToLuaTableCaller(
+            long luaEnginePtr, PersistableBundle bundle);
 
     // Creates an instance of LuaEngine on the heap and returns the pointer.
     private native long nativeCreateLuaEngine();
@@ -91,7 +92,7 @@
 
     @Test
     public void pushBundleToLuaTable_valuesOfDifferentTypes() {
-        Bundle bundle = new Bundle();
+        PersistableBundle bundle = new PersistableBundle();
         bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
         bundle.putInt(INT_KEY, INT_VALUE);
         bundle.putDouble(NUMBER_KEY, NUMBER_VALUE);
@@ -111,7 +112,7 @@
 
     @Test
     public void pushBundleToLuaTable_wrongKey() {
-        Bundle bundle = new Bundle();
+        PersistableBundle bundle = new PersistableBundle();
         bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
 
         // Invokes the corresponding helper method to convert the bundle
diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/ScriptExecutorTest.java b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/ScriptExecutorTest.java
index 5d75766..37760f0 100644
--- a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/ScriptExecutorTest.java
+++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/ScriptExecutorTest.java
@@ -24,12 +24,17 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.os.Bundle;
 import android.os.IBinder;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
+import android.os.UserHandle;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorConstants;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -47,21 +52,23 @@
 
 
     private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
-        public Bundle mSavedBundle;
+        public PersistableBundle mSavedBundle;
+        public PersistableBundle mFinalResult;
         public int mErrorType;
         public String mMessage;
         public String mStackTrace;
-        public final CountDownLatch mSuccessLatch = new CountDownLatch(1);
-        public final CountDownLatch mErrorLatch = new CountDownLatch(1);
+        public final CountDownLatch mResponseLatch = new CountDownLatch(1);
 
         @Override
-        public void onScriptFinished(byte[] result) {
+        public void onScriptFinished(PersistableBundle result) {
+            mFinalResult = result;
+            mResponseLatch.countDown();
         }
 
         @Override
-        public void onSuccess(Bundle stateToPersist) {
+        public void onSuccess(PersistableBundle stateToPersist) {
             mSavedBundle = stateToPersist;
-            mSuccessLatch.countDown();
+            mResponseLatch.countDown();
         }
 
         @Override
@@ -69,7 +76,7 @@
             mErrorType = errorType;
             mMessage = message;
             mStackTrace = stackTrace;
-            mErrorLatch.countDown();
+            mResponseLatch.countDown();
         }
     }
 
@@ -78,8 +85,8 @@
 
     // TODO(b/189241508). Parsing of publishedData is not implemented yet.
     // Null is the only accepted input.
-    private final Bundle mPublishedData = null;
-    private final Bundle mSavedState = new Bundle();
+    private final PersistableBundle mPublishedData = null;
+    private final PersistableBundle mSavedState = new PersistableBundle();
 
     private static final String LUA_SCRIPT =
             "function hello(state)\n"
@@ -91,8 +98,7 @@
     private final CountDownLatch mBindLatch = new CountDownLatch(1);
 
     private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
-    private static final int SCRIPT_SUCCESS_TIMEOUT_SEC = 10;
-    private static final int SCRIPT_ERROR_TIMEOUT_SEC = 10;
+    private static final int SCRIPT_PROCESSING_TIMEOUT_SEC = 10;
 
 
     private final ServiceConnection mScriptExecutorConnection =
@@ -109,15 +115,16 @@
                 }
             };
 
-    // Helper method to invoke the script and wait for it to complete and return the result.
-    private void runScriptAndWaitForResult(String script, String function, Bundle previousState)
+    // Helper method to invoke the script and wait for it to complete and return a response.
+    private void runScriptAndWaitForResponse(String script, String function,
+            PersistableBundle previousState)
             throws RemoteException {
         mScriptExecutor.invokeScript(script, function, mPublishedData, previousState,
                 mFakeScriptExecutorListener);
         try {
-            if (!mFakeScriptExecutorListener.mSuccessLatch.await(SCRIPT_SUCCESS_TIMEOUT_SEC,
+            if (!mFakeScriptExecutorListener.mResponseLatch.await(SCRIPT_PROCESSING_TIMEOUT_SEC,
                     TimeUnit.SECONDS)) {
-                fail("Failed to get on_success called by the script on time");
+                fail("Failed to get the callback method called by the script on time");
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
@@ -125,18 +132,9 @@
         }
     }
 
+
     private void runScriptAndWaitForError(String script, String function) throws RemoteException {
-        mScriptExecutor.invokeScript(script, function, mPublishedData, new Bundle(),
-                mFakeScriptExecutorListener);
-        try {
-            if (!mFakeScriptExecutorListener.mErrorLatch.await(SCRIPT_ERROR_TIMEOUT_SEC,
-                    TimeUnit.SECONDS)) {
-                fail("Failed to get on_error called by the script on time");
-            }
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-            fail(e.getMessage());
-        }
+        runScriptAndWaitForResponse(script, function, new PersistableBundle());
     }
 
     @Before
@@ -144,7 +142,8 @@
         Intent intent = new Intent();
         intent.setComponent(new ComponentName("com.android.car.scriptexecutor",
                 "com.android.car.scriptexecutor.ScriptExecutor"));
-        mContext.bindService(intent, mScriptExecutorConnection, Context.BIND_AUTO_CREATE);
+        mContext.bindServiceAsUser(intent, mScriptExecutorConnection, Context.BIND_AUTO_CREATE,
+                UserHandle.SYSTEM);
         if (!mBindLatch.await(BIND_SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
             fail("Failed to bind to ScriptExecutor service");
         }
@@ -176,7 +175,7 @@
                         + "end\n";
 
 
-        runScriptAndWaitForResult(returnResultScript, "hello", mSavedState);
+        runScriptAndWaitForResponse(returnResultScript, "hello", mSavedState);
 
         // Expect to get back a bundle with a single string key: string value pair:
         // {"hello": "world"}
@@ -193,7 +192,7 @@
                         + "end\n";
 
 
-        runScriptAndWaitForResult(script, "knows", mSavedState);
+        runScriptAndWaitForResponse(script, "knows", mSavedState);
 
         // Expect to get back a bundle with 4 keys, each corresponding to a distinct supported type.
         assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
@@ -213,7 +212,7 @@
                         + "end\n";
 
 
-        runScriptAndWaitForResult(script, "nested", mSavedState);
+        runScriptAndWaitForResponse(script, "nested", mSavedState);
 
         // Bundle does not contain any value under "nested_table" key, because nested tables are
         // not supported yet.
@@ -230,7 +229,7 @@
                         + "end\n";
 
 
-        runScriptAndWaitForResult(script, "empty", mSavedState);
+        runScriptAndWaitForResponse(script, "empty", mSavedState);
 
         // If a script returns empty table as the result, we get an empty bundle.
         assertThat(mFakeScriptExecutorListener.mSavedBundle).isNotNull();
@@ -247,11 +246,11 @@
                         + "    result = {y = state.x+1}\n"
                         + "    on_success(result)\n"
                         + "end\n";
-        Bundle previousState = new Bundle();
+        PersistableBundle previousState = new PersistableBundle();
         previousState.putInt("x", 1);
 
 
-        runScriptAndWaitForResult(script, "update", previousState);
+        runScriptAndWaitForResponse(script, "update", previousState);
 
         // Verify that y = 2, because y = x + 1 and x = 1.
         assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
@@ -273,14 +272,14 @@
                         + "    result.string = state.string .. \"CADABRA\"\n"
                         + "    on_success(result)\n"
                         + "end\n";
-        Bundle previousState = new Bundle();
+        PersistableBundle previousState = new PersistableBundle();
         previousState.putInt("integer", 1);
         previousState.putDouble("number", 0.1);
         previousState.putBoolean("boolean", false);
         previousState.putString("string", "ABRA");
 
 
-        runScriptAndWaitForResult(script, "update_all", previousState);
+        runScriptAndWaitForResponse(script, "update_all", previousState);
 
         // Verify that keys are preserved but the values are modified as expected.
         assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
@@ -343,5 +342,151 @@
         assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
                 "on_error can push only a single string parameter from Lua");
     }
+
+    @Test
+    public void invokeScript_returnsFinalResult() throws RemoteException {
+        String returnFinalResultScript =
+                "function script_finishes(state)\n"
+                        + "    result = {data = state.input + 1}\n"
+                        + "    on_script_finished(result)\n"
+                        + "end\n";
+        PersistableBundle previousState = new PersistableBundle();
+        previousState.putInt("input", 1);
+
+        runScriptAndWaitForResponse(returnFinalResultScript, "script_finishes", previousState);
+
+        // Expect to get back a bundle with a single key-value pair {"data": 2}
+        // because data = state.input + 1 as in the script body above.
+        assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mFinalResult.getInt("data")).isEqualTo(2);
+    }
+
+    @Test
+    public void invokeScript_allSupportedTypesForReturningFinalResult()
+            throws RemoteException {
+        // Here we verify that all supported types are present in the returned final result
+        // bundle are present.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function finalize_all(state)\n"
+                        + "    result = {}\n"
+                        + "    result.integer = state.integer + 1\n"
+                        + "    result.number = state.number + 0.1\n"
+                        + "    result.boolean = not state.boolean\n"
+                        + "    result.string = state.string .. \"CADABRA\"\n"
+                        + "    on_script_finished(result)\n"
+                        + "end\n";
+        PersistableBundle previousState = new PersistableBundle();
+        previousState.putInt("integer", 1);
+        previousState.putDouble("number", 0.1);
+        previousState.putBoolean("boolean", false);
+        previousState.putString("string", "ABRA");
+
+
+        runScriptAndWaitForResponse(script, "finalize_all", previousState);
+
+        // Verify that keys are preserved but the values are modified as expected.
+        assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(4);
+        assertThat(mFakeScriptExecutorListener.mFinalResult.getInt("integer")).isEqualTo(2);
+        assertThat(mFakeScriptExecutorListener.mFinalResult.getDouble("number")).isEqualTo(0.2);
+        assertThat(mFakeScriptExecutorListener.mFinalResult.getBoolean("boolean")).isEqualTo(true);
+        assertThat(mFakeScriptExecutorListener.mFinalResult.getString("string")).isEqualTo(
+                "ABRACADABRA");
+    }
+
+    @Test
+    public void invokeScript_emptyFinalResultBundle() throws RemoteException {
+        String script =
+                "function empty_final_result(state)\n"
+                        + "    result = {}\n"
+                        + "    on_script_finished(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResponse(script, "empty_final_result", mSavedState);
+
+        // If a script returns empty table as the final result, we get an empty bundle.
+        assertThat(mFakeScriptExecutorListener.mFinalResult).isNotNull();
+        assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void invokeScript_wrongNumberOfCallbackInputsInOnScriptFinished()
+            throws RemoteException {
+        String script =
+                "function wrong_number_of_outputs_in_on_script_finished(state)\n"
+                        + "    result = {}\n"
+                        + "    extra = 1\n"
+                        + "    on_script_finished(result, extra)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResponse(script, "wrong_number_of_outputs_in_on_script_finished",
+                mSavedState);
+
+        // We expect to get an error here because we expect only 1 input parameter in
+        // on_script_finished.
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+                "on_script_finished can push only a single parameter from Lua - a Lua table");
+    }
+
+    @Test
+    public void invokeScript_wrongNumberOfCallbackInputsInOnSuccess() throws RemoteException {
+        String script =
+                "function wrong_number_of_outputs_in_on_success(state)\n"
+                        + "    result = {}\n"
+                        + "    extra = 1\n"
+                        + "    on_success(result, extra)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResponse(script, "wrong_number_of_outputs_in_on_success", mSavedState);
+
+        // We expect to get an error here because we expect only 1 input parameter in on_success.
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+                "on_success can push only a single parameter from Lua - a Lua table");
+    }
+
+    @Test
+    public void invokeScript_wrongTypeInOnSuccess() throws RemoteException {
+        String script =
+                "function wrong_type_in_on_success(state)\n"
+                        + "    result = 1\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResponse(script, "wrong_type_in_on_success", mSavedState);
+
+        // We expect to get an error here because the type of the input parameter for on_success
+        // must be a Lua table.
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+                "on_success can push only a single parameter from Lua - a Lua table");
+    }
+
+    @Test
+    public void invokeScript_wrongTypeInOnScriptFinished() throws RemoteException {
+        String script =
+                "function wrong_type_in_on_script_finished(state)\n"
+                        + "    result = 1\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResponse(script, "wrong_type_in_on_script_finished", mSavedState);
+
+        // We expect to get an error here because the type of the input parameter for
+        // on_script_finished must be a Lua table.
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+                "on_success can push only a single parameter from Lua - a Lua table");
+    }
 }
 
diff --git a/service/Android.bp b/service/Android.bp
index c0bfb51..7f4a329 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -29,6 +29,7 @@
 }
 
 car_service_sources = [
+    ":iscriptexecutor_aidl",
     "src/**/*.java",
     ":statslog-Car-java-gen",
 ]
@@ -50,7 +51,6 @@
     "vehicle-hal-support-lib",
     "car-systemtest",
     "com.android.car.procfsinspector-client",
-    "com.android.car.scriptexecutor-interface-lib",
     "blestream-protos",
     "SettingsLib",
     "androidx.preference_preference",
@@ -89,6 +89,12 @@
         "libcarservicejni",
     ],
 
+    aidl: {
+        include_dirs: [
+	    "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+	],
+    },
+
     required: ["allowed_privapp_com.android.car"],
 
     // Disable build in PDK, missing aidl import breaks build
@@ -139,6 +145,12 @@
 
     static_libs: common_lib_deps,
 
+    aidl: {
+        include_dirs: [
+	    "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+	],
+    },
+
     product_variables: {
         pdk: {
             enabled: false,
@@ -163,9 +175,32 @@
         "car-frameworks-service",
     ],
 
+    aidl: {
+        include_dirs: [
+	    "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+	],
+    },
+
     product_variables: {
         pdk: {
             enabled: false,
         },
     },
 }
+
+filegroup {
+    name: "iscriptexecutor_aidl",
+    srcs: [
+        "src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl",
+        "src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl",
+    ],
+    path: "src",
+}
+
+filegroup {
+    name: "iscriptexecutorconstants_aidl",
+    srcs: [
+        "src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.aidl",
+    ],
+    path: "src",
+}
diff --git a/service/src/com/android/car/CarInputService.java b/service/src/com/android/car/CarInputService.java
index d4d8e35..f63a6e0 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -449,6 +449,12 @@
                 InputDevice.SOURCE_CLASS_BUTTON);
     }
 
+    /**
+     * Requests capturing of input event for the specified display for all requested input types.
+     *
+     * Currently this method requires {@code android.car.permission.CAR_MONITOR_INPUT} or
+     * {@code android.permission.MONITOR_INPUT} permissions (any of them will be acceptable).
+     */
     @Override
     public int requestInputEventCapture(ICarInputCallback callback,
             @DisplayTypeEnum int targetDisplayType,
@@ -457,6 +463,13 @@
                 requestFlags);
     }
 
+    /**
+     * Overloads #requestInputEventCapture(int, int[], int, CarInputCaptureCallback) by providing
+     * a {@link java.util.concurrent.Executor} to be used when invoking the callback argument.
+     *
+     * Currently this method requires {@code android.car.permission.CAR_MONITOR_INPUT} or
+     * {@code android.permission.MONITOR_INPUT} permissions (any of them will be acceptable).
+     */
     @Override
     public void releaseInputEventCapture(ICarInputCallback callback,
             @DisplayTypeEnum int targetDisplayType) {
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index df600de..d590083 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -354,7 +354,7 @@
         }
 
         if (mFeatureController.isFeatureEnabled(Car.CAR_TELEMETRY_SERVICE)) {
-            mCarTelemetryService = new CarTelemetryService(serviceContext);
+            mCarTelemetryService = new CarTelemetryService(serviceContext, mCarPropertyService);
         } else {
             mCarTelemetryService = null;
         }
diff --git a/service/src/com/android/car/InputCaptureClientController.java b/service/src/com/android/car/InputCaptureClientController.java
index 7719b24..5f9ebfa 100644
--- a/service/src/com/android/car/InputCaptureClientController.java
+++ b/service/src/com/android/car/InputCaptureClientController.java
@@ -246,7 +246,8 @@
     public int requestInputEventCapture(ICarInputCallback callback,
             @DisplayTypeEnum int targetDisplayType,
             int[] inputTypes, int requestFlags) {
-        ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_MONITOR_INPUT);
+        ICarImpl.assertAnyPermission(mContext, Car.PERMISSION_CAR_MONITOR_INPUT,
+                android.Manifest.permission.MONITOR_INPUT);
 
         Preconditions.checkArgument(SUPPORTED_DISPLAY_TYPES.contains(targetDisplayType),
                 "Display not supported yet:" + targetDisplayType);
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index f6a98d6..1673f61 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -69,7 +69,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-import android.view.KeyEvent;
 
 import com.android.car.CarLocalServices;
 import com.android.car.CarLog;
@@ -436,25 +435,8 @@
     void setMasterMute(boolean mute, int flags) {
         mAudioManager.setMasterMute(mute, flags);
 
-        // Master Mute only appliers to primary zone
+        // Master Mute only applies to primary zone
         callbackMasterMuteChange(PRIMARY_AUDIO_ZONE, flags);
-
-        // When the master mute is turned ON, we want the playing app to get a "pause" command.
-        // When the volume is unmuted, we want to resume playback.
-        int keycode = mute ? KeyEvent.KEYCODE_MEDIA_PAUSE : KeyEvent.KEYCODE_MEDIA_PLAY;
-
-        dispatchMediaKeyEvent(keycode);
-    }
-
-    private void dispatchMediaKeyEvent(int keycode) {
-        long currentTime = SystemClock.uptimeMillis();
-        KeyEvent keyDown = new KeyEvent(/* downTime= */ currentTime, /* eventTime= */ currentTime,
-                KeyEvent.ACTION_DOWN, keycode, /* repeat= */ 0);
-        mAudioManager.dispatchMediaKeyEvent(keyDown);
-
-        KeyEvent keyUp = new KeyEvent(/* downTime= */ currentTime, /* eventTime= */ currentTime,
-                KeyEvent.ACTION_UP, keycode, /* repeat= */ 0);
-        mAudioManager.dispatchMediaKeyEvent(keyUp);
     }
 
     void callbackMasterMuteChange(int zoneId, int flags) {
diff --git a/service/src/com/android/car/pm/CarSafetyAccessibilityService.java b/service/src/com/android/car/pm/CarSafetyAccessibilityService.java
index 787a0f8..76a6246 100644
--- a/service/src/com/android/car/pm/CarSafetyAccessibilityService.java
+++ b/service/src/com/android/car/pm/CarSafetyAccessibilityService.java
@@ -30,7 +30,9 @@
     public void onAccessibilityEvent(AccessibilityEvent event) {
         CarPackageManagerService carPackageManagerService =
                 CarLocalServices.getService(CarPackageManagerService.class);
-        carPackageManagerService.onWindowChangeEvent();
+        if (carPackageManagerService != null) {
+            carPackageManagerService.onWindowChangeEvent();
+        }
     }
 
     @Override
diff --git a/service/src/com/android/car/telemetry/CarTelemetryService.java b/service/src/com/android/car/telemetry/CarTelemetryService.java
index 695e25e..5b78e3b 100644
--- a/service/src/com/android/car/telemetry/CarTelemetryService.java
+++ b/service/src/com/android/car/telemetry/CarTelemetryService.java
@@ -15,28 +15,41 @@
  */
 package com.android.car.telemetry;
 
-import static android.car.telemetry.CarTelemetryManager.ERROR_NEWER_MANIFEST_EXISTS;
-import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
-import static android.car.telemetry.CarTelemetryManager.ERROR_PARSE_MANIFEST_FAILED;
-import static android.car.telemetry.CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_PARSE_FAILED;
 
 import android.annotation.NonNull;
+import android.app.StatsManager;
 import android.car.Car;
-import android.car.telemetry.CarTelemetryManager.AddManifestError;
 import android.car.telemetry.ICarTelemetryService;
 import android.car.telemetry.ICarTelemetryServiceListener;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.MetricsConfigKey;
 import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.RemoteException;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 
+import com.android.car.CarLocalServices;
+import com.android.car.CarLog;
+import com.android.car.CarPropertyService;
 import com.android.car.CarServiceBase;
-import com.android.internal.annotations.GuardedBy;
+import com.android.car.CarServiceUtils;
+import com.android.car.systeminterface.SystemInterface;
+import com.android.car.telemetry.databroker.DataBroker;
+import com.android.car.telemetry.databroker.DataBrokerController;
+import com.android.car.telemetry.databroker.DataBrokerImpl;
+import com.android.car.telemetry.publisher.PublisherFactory;
+import com.android.car.telemetry.publisher.StatsManagerImpl;
+import com.android.car.telemetry.publisher.StatsManagerProxy;
+import com.android.car.telemetry.systemmonitor.SystemMonitor;
+import com.android.internal.annotations.VisibleForTesting;
 
 import com.google.protobuf.InvalidProtocolBufferException;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.io.File;
+import java.util.List;
 
 /**
  * CarTelemetryService manages OEM telemetry collection, processing and communication
@@ -44,32 +57,62 @@
  */
 public class CarTelemetryService extends ICarTelemetryService.Stub implements CarServiceBase {
 
-    // TODO(b/189340793): Rename Manifest to MetricsConfig
-
     private static final boolean DEBUG = false;
-    private static final int DEFAULT_VERSION = 0;
-    private static final String TAG = CarTelemetryService.class.getSimpleName();
+    public static final String TELEMETRY_DIR = "telemetry";
 
     private final Context mContext;
-    @GuardedBy("mLock")
-    private final Map<String, Integer> mNameVersionMap = new HashMap<>();
-    private final Object mLock = new Object();
+    private final CarPropertyService mCarPropertyService;
+    private final File mRootDirectory;
+    private final HandlerThread mTelemetryThread = CarServiceUtils.getHandlerThread(
+            CarTelemetryService.class.getSimpleName());
+    private final Handler mTelemetryHandler = new Handler(mTelemetryThread.getLooper());
 
-    @GuardedBy("mLock")
     private ICarTelemetryServiceListener mListener;
+    private DataBroker mDataBroker;
+    private DataBrokerController mDataBrokerController;
+    private MetricsConfigStore mMetricsConfigStore;
+    private PublisherFactory mPublisherFactory;
+    private ResultStore mResultStore;
+    private SharedPreferences mSharedPrefs;
+    private StatsManagerProxy mStatsManagerProxy;
+    private SystemMonitor mSystemMonitor;
 
-    public CarTelemetryService(Context context) {
+    public CarTelemetryService(Context context, CarPropertyService carPropertyService) {
         mContext = context;
+        mCarPropertyService = carPropertyService;
+        SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class);
+        // full root directory path is /data/system/car/telemetry
+        mRootDirectory = new File(systemInterface.getSystemCarDir(), TELEMETRY_DIR);
     }
 
     @Override
     public void init() {
-        // nothing to do
+        mTelemetryHandler.post(() -> {
+            // initialize all necessary components
+            mMetricsConfigStore = new MetricsConfigStore(mRootDirectory);
+            mResultStore = new ResultStore(mRootDirectory);
+            mStatsManagerProxy = new StatsManagerImpl(
+                    mContext.getSystemService(StatsManager.class));
+            // TODO(b/197968695): delay initialization of stats publisher
+            mPublisherFactory = new PublisherFactory(mCarPropertyService, mTelemetryHandler,
+                    mStatsManagerProxy, null);
+            mDataBroker = new DataBrokerImpl(mContext, mPublisherFactory, mResultStore);
+            mSystemMonitor = SystemMonitor.create(mContext, mTelemetryHandler);
+            mDataBrokerController = new DataBrokerController(mDataBroker, mSystemMonitor);
+
+            // start collecting data. once data is sent by publisher, scripts will be able to run
+            List<TelemetryProto.MetricsConfig> activeConfigs =
+                    mMetricsConfigStore.getActiveMetricsConfigs();
+            for (TelemetryProto.MetricsConfig config : activeConfigs) {
+                mDataBroker.addMetricsConfiguration(config);
+            }
+        });
     }
 
     @Override
     public void release() {
-        // nothing to do
+        // TODO(b/197969149): prevent threading issue, block main thread
+        mTelemetryHandler.post(() -> mResultStore.flushToDisk());
     }
 
     @Override
@@ -85,9 +128,12 @@
         // TODO(b/184890506): verify that only a hardcoded app can set the listener
         mContext.enforceCallingOrSelfPermission(
                 Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
-        synchronized (mLock) {
-            setListenerLocked(listener);
-        }
+        mTelemetryHandler.post(() -> {
+            if (DEBUG) {
+                Slog.d(CarLog.TAG_TELEMETRY, "Setting the listener for car telemetry service");
+            }
+            mListener = listener;
+        });
     }
 
     /**
@@ -96,50 +142,80 @@
     @Override
     public void clearListener() {
         mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
-        synchronized (mLock) {
-            clearListenerLocked();
-        }
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "clearListener");
+        mTelemetryHandler.post(() -> {
+            if (DEBUG) {
+                Slog.d(CarLog.TAG_TELEMETRY, "Clearing the listener for car telemetry service");
+            }
+            mListener = null;
+        });
     }
 
     /**
-     * Allows client to send telemetry manifests.
+     * Send a telemetry metrics config to the service.
      *
-     * @param key    the unique key to identify the manifest.
-     * @param config the serialized bytes of a Manifest object.
-     * @return {@link AddManifestError} the error code.
+     * @param key    the unique key to identify the MetricsConfig.
+     * @param config the serialized bytes of a MetricsConfig object.
      */
     @Override
-    public @AddManifestError int addManifest(@NonNull ManifestKey key, @NonNull byte[] config) {
+    public void addMetricsConfig(@NonNull MetricsConfigKey key, @NonNull byte[] config) {
         mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
-        synchronized (mLock) {
-            return addManifestLocked(key, config);
-        }
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "addMetricsConfig");
+        // TODO(b/198797367): if an older version exists, delete its script result/error
+        mTelemetryHandler.post(() -> {
+            TelemetryProto.MetricsConfig metricsConfig;
+            int status;
+            try {
+                metricsConfig = TelemetryProto.MetricsConfig.parseFrom(config);
+                status = mMetricsConfigStore.addMetricsConfig(metricsConfig);
+            } catch (InvalidProtocolBufferException e) {
+                Slog.e(CarLog.TAG_TELEMETRY, "Failed to parse MetricsConfig.", e);
+                status = ERROR_METRICS_CONFIG_PARSE_FAILED;
+            }
+            try {
+                mListener.onAddMetricsConfigStatus(key, status);
+            } catch (RemoteException e) {
+                Slog.d(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
+            }
+        });
     }
 
     /**
      * Removes a manifest based on the key.
      */
     @Override
-    public boolean removeManifest(@NonNull ManifestKey key) {
+    public void removeMetricsConfig(@NonNull MetricsConfigKey key) {
         mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
-        synchronized (mLock) {
-            return removeManifestLocked(key);
-        }
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "removeMetricsConfig");
+        // TODO(b/198797367): Delete script result/error associated with this MetricsConfig
+        mTelemetryHandler.post(() -> {
+            if (DEBUG) {
+                Slog.d(CarLog.TAG_TELEMETRY, "Removing manifest " + key.getName()
+                        + " from car telemetry service");
+            }
+            // TODO(b/198792767): Check both config name and config version for deletion
+            boolean success = mMetricsConfigStore.deleteMetricsConfig(key.getName());
+            try {
+                mListener.onRemoveMetricsConfigStatus(key, success);
+            } catch (RemoteException e) {
+                Slog.d(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
+            }
+        });
     }
 
     /**
      * Removes all manifests.
      */
     @Override
-    public void removeAllManifests() {
+    public void removeAllMetricsConfigs() {
         mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
-        synchronized (mLock) {
-            removeAllManifestsLocked();
-        }
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "removeAllMetricsConfig");
+        mTelemetryHandler.post(() -> {
+            if (DEBUG) {
+                Slog.d(CarLog.TAG_TELEMETRY, "Removing all manifest from car telemetry service");
+            }
+            // TODO(b/184087869): Implement
+        });
     }
 
     /**
@@ -147,99 +223,30 @@
      * {@link ICarTelemetryServiceListener}.
      */
     @Override
-    public void sendFinishedReports(@NonNull ManifestKey key) {
+    public void sendFinishedReports(@NonNull MetricsConfigKey key) {
         // TODO(b/184087869): Implement
         mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "sendFinishedReports");
         if (DEBUG) {
-            Slog.d(TAG, "Flushing reports for a manifest");
+            Slog.d(CarLog.TAG_TELEMETRY, "Flushing reports for a manifest");
         }
     }
 
     /**
-     * Sends all script results associated using the {@link ICarTelemetryServiceListener}.
+     * Sends all script results or errors using the {@link ICarTelemetryServiceListener}.
      */
     @Override
     public void sendAllFinishedReports() {
         // TODO(b/184087869): Implement
         mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "sendAllFinishedReports");
         if (DEBUG) {
-            Slog.d(TAG, "Flushing all reports");
+            Slog.d(CarLog.TAG_TELEMETRY, "Flushing all reports");
         }
     }
 
-    /**
-     * Sends all errors using the {@link ICarTelemetryServiceListener}.
-     */
-    @Override
-    public void sendScriptExecutionErrors() {
-        // TODO(b/184087869): Implement
-        mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
-        if (DEBUG) {
-            Slog.d(TAG, "Flushing script execution errors");
-        }
-    }
-
-    @GuardedBy("mLock")
-    private void setListenerLocked(@NonNull ICarTelemetryServiceListener listener) {
-        if (DEBUG) {
-            Slog.d(TAG, "Setting the listener for car telemetry service");
-        }
-        mListener = listener;
-    }
-
-    @GuardedBy("mLock")
-    private void clearListenerLocked() {
-        if (DEBUG) {
-            Slog.d(TAG, "Clearing listener");
-        }
-        mListener = null;
-    }
-
-    @GuardedBy("mLock")
-    private @AddManifestError int addManifestLocked(ManifestKey key, byte[] configProto) {
-        if (DEBUG) {
-            Slog.d(TAG, "Adding MetricsConfig to car telemetry service");
-        }
-        int currentVersion = mNameVersionMap.getOrDefault(key.getName(), DEFAULT_VERSION);
-        if (currentVersion > key.getVersion()) {
-            return ERROR_NEWER_MANIFEST_EXISTS;
-        } else if (currentVersion == key.getVersion()) {
-            return ERROR_SAME_MANIFEST_EXISTS;
-        }
-
-        TelemetryProto.MetricsConfig metricsConfig;
-        try {
-            metricsConfig = TelemetryProto.MetricsConfig.parseFrom(configProto);
-        } catch (InvalidProtocolBufferException e) {
-            Slog.e(TAG, "Failed to parse MetricsConfig.", e);
-            return ERROR_PARSE_MANIFEST_FAILED;
-        }
-        mNameVersionMap.put(key.getName(), key.getVersion());
-
-        // TODO(b/186047142): Store the MetricsConfig to disk
-        // TODO(b/186047142): Send metricsConfig to a script manager or a queue
-        return ERROR_NONE;
-    }
-
-    @GuardedBy("mLock")
-    private boolean removeManifestLocked(@NonNull ManifestKey key) {
-        Integer version = mNameVersionMap.remove(key.getName());
-        if (version == null) {
-            return false;
-        }
-        // TODO(b/186047142): Delete manifest from disk and remove it from queue
-        return true;
-    }
-
-    @GuardedBy("mLock")
-    private void removeAllManifestsLocked() {
-        if (DEBUG) {
-            Slog.d(TAG, "Removing all manifest from car telemetry service");
-        }
-        mNameVersionMap.clear();
-        // TODO(b/186047142): Delete all manifests from disk & queue
+    @VisibleForTesting
+    Handler getTelemetryHandler() {
+        return mTelemetryHandler;
     }
 }
diff --git a/service/src/com/android/car/telemetry/MetricsConfigStore.java b/service/src/com/android/car/telemetry/MetricsConfigStore.java
new file mode 100644
index 0000000..9802e0e
--- /dev/null
+++ b/service/src/com/android/car/telemetry/MetricsConfigStore.java
@@ -0,0 +1,114 @@
+/*
+ * 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.telemetry;
+
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_ALREADY_EXISTS;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_NONE;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_UNKNOWN;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_VERSION_TOO_OLD;
+
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import com.android.car.CarLog;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is responsible for storing, retrieving, and deleting {@link
+ * TelemetryProto.MetricsConfig}. All of the methods are blocking so the class should only be
+ * accessed on the telemetry thread.
+ */
+class MetricsConfigStore {
+    @VisibleForTesting
+    static final String METRICS_CONFIG_DIR = "metrics_configs";
+
+    private final File mConfigDirectory;
+    private Map<String, TelemetryProto.MetricsConfig> mActiveConfigs;
+    private Map<String, Integer> mNameVersionMap;
+
+    MetricsConfigStore(File rootDirectory) {
+        mConfigDirectory = new File(rootDirectory, METRICS_CONFIG_DIR);
+        mConfigDirectory.mkdirs();
+        mActiveConfigs = new ArrayMap<>();
+        mNameVersionMap = new ArrayMap<>();
+        // TODO(b/197336485): Add expiration date check for MetricsConfig
+        for (File file : mConfigDirectory.listFiles()) {
+            try {
+                byte[] serializedConfig = Files.readAllBytes(file.toPath());
+                TelemetryProto.MetricsConfig config =
+                        TelemetryProto.MetricsConfig.parseFrom(serializedConfig);
+                mActiveConfigs.put(config.getName(), config);
+                mNameVersionMap.put(config.getName(), config.getVersion());
+            } catch (IOException e) {
+                // TODO(b/197336655): record failure
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Returns all active {@link TelemetryProto.MetricsConfig} from disk.
+     */
+    List<TelemetryProto.MetricsConfig> getActiveMetricsConfigs() {
+        return new ArrayList<>(mActiveConfigs.values());
+    }
+
+    /**
+     * Stores the MetricsConfig if it is valid.
+     *
+     * @param metricsConfig the config to be persisted to disk.
+     * @return true if the MetricsConfig should start receiving data, false otherwise.
+     */
+    int addMetricsConfig(TelemetryProto.MetricsConfig metricsConfig) {
+        // TODO(b/198823862): Validate config version
+        // TODO(b/197336485): Check expiration date for MetricsConfig
+        int currentVersion = mNameVersionMap.getOrDefault(metricsConfig.getName(), -1);
+        if (currentVersion > metricsConfig.getVersion()) {
+            return ERROR_METRICS_CONFIG_VERSION_TOO_OLD;
+        } else if (currentVersion == metricsConfig.getVersion()) {
+            return ERROR_METRICS_CONFIG_ALREADY_EXISTS;
+        }
+        mNameVersionMap.put(metricsConfig.getName(), metricsConfig.getVersion());
+        try {
+            Files.write(
+                    new File(mConfigDirectory, metricsConfig.getName()).toPath(),
+                    metricsConfig.toByteArray());
+        } catch (IOException e) {
+            // TODO(b/197336655): record failure
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to write metrics config to disk", e);
+            return ERROR_METRICS_CONFIG_UNKNOWN;
+        }
+        return ERROR_METRICS_CONFIG_NONE;
+    }
+
+    /** Deletes the MetricsConfig from disk. Returns the success status. */
+    boolean deleteMetricsConfig(String metricsConfigName) {
+        mActiveConfigs.remove(metricsConfigName);
+        return new File(mConfigDirectory, metricsConfigName).delete();
+    }
+
+    void deleteAllMetricsConfig() {
+        // TODO(b/198784116): implement
+    }
+}
diff --git a/service/src/com/android/car/telemetry/ResultStore.java b/service/src/com/android/car/telemetry/ResultStore.java
index 27cb50a..976ee8e 100644
--- a/service/src/com/android/car/telemetry/ResultStore.java
+++ b/service/src/com/android/car/telemetry/ResultStore.java
@@ -16,25 +16,23 @@
 
 package com.android.car.telemetry;
 
-import android.os.Handler;
 import android.os.PersistableBundle;
 import android.util.ArrayMap;
 import android.util.Slog;
 
 import com.android.car.CarLog;
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
-import java.nio.file.Files;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 /**
  * Disk storage for interim and final metrics statistics.
+ * All methods in this class should be invoked from the telemetry thread.
  */
 public class ResultStore {
 
@@ -45,32 +43,23 @@
     @VisibleForTesting
     static final String FINAL_RESULT_DIR = "final";
 
-    private final Object mLock = new Object();
     /** Map keys are MetricsConfig names, which are also the file names in disk. */
-    @GuardedBy("mLock")
     private final Map<String, InterimResult> mInterimResultCache = new ArrayMap<>();
 
     private final File mInterimResultDirectory;
     private final File mFinalResultDirectory;
-    private final Handler mWorkerHandler; // for all non I/O operations
-    private final Handler mIoHandler; // for all I/O operations
 
-    ResultStore(Handler handler, Handler ioHandler, File rootDirectory) {
-        mWorkerHandler = handler;
-        mIoHandler = ioHandler;
+    ResultStore(File rootDirectory) {
         mInterimResultDirectory = new File(rootDirectory, INTERIM_RESULT_DIR);
         mFinalResultDirectory = new File(rootDirectory, FINAL_RESULT_DIR);
         mInterimResultDirectory.mkdirs();
         mFinalResultDirectory.mkdirs();
         // load results into memory to reduce the frequency of disk access
-        synchronized (mLock) {
-            loadInterimResultsIntoMemoryLocked();
-        }
+        loadInterimResultsIntoMemory();
     }
 
     /** Reads interim results into memory for faster access. */
-    @GuardedBy("mLock")
-    private void loadInterimResultsIntoMemoryLocked() {
+    private void loadInterimResultsIntoMemory() {
         for (File file : mInterimResultDirectory.listFiles()) {
             try (FileInputStream fis = new FileInputStream(file)) {
                 mInterimResultCache.put(
@@ -88,12 +77,10 @@
      * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
      */
     public PersistableBundle getInterimResult(String metricsConfigName) {
-        synchronized (mLock) {
-            if (!mInterimResultCache.containsKey(metricsConfigName)) {
-                return null;
-            }
-            return mInterimResultCache.get(metricsConfigName).getBundle();
+        if (!mInterimResultCache.containsKey(metricsConfigName)) {
+            return null;
         }
+        return mInterimResultCache.get(metricsConfigName).getBundle();
     }
 
     /**
@@ -101,11 +88,7 @@
      * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
      */
     public void putInterimResult(String metricsConfigName, PersistableBundle result) {
-        synchronized (mLock) {
-            mInterimResultCache.put(
-                    metricsConfigName,
-                    new InterimResult(result, /* dirty = */ true));
-        }
+        mInterimResultCache.put(metricsConfigName, new InterimResult(result, /* dirty = */ true));
     }
 
     /**
@@ -119,30 +102,18 @@
      */
     public void getFinalResult(
             String metricsConfigName, boolean deleteResult, FinalResultCallback callback) {
-        // I/O operations should happen on I/O thread
-        mIoHandler.post(() -> {
-            synchronized (mLock) {
-                loadFinalResultLockedOnIoThread(metricsConfigName, deleteResult, callback);
-            }
-        });
-    }
-
-    @GuardedBy("mLock")
-    private void loadFinalResultLockedOnIoThread(
-            String metricsConfigName, boolean deleteResult, FinalResultCallback callback) {
         File file = new File(mFinalResultDirectory, metricsConfigName);
         // if no final result exists for this metrics config, return immediately
         if (!file.exists()) {
-            mWorkerHandler.post(() -> callback.onFinalResult(metricsConfigName, null));
+            callback.onFinalResult(metricsConfigName, null);
             return;
         }
         try (FileInputStream fis = new FileInputStream(file)) {
             PersistableBundle bundle = PersistableBundle.readFromStream(fis);
-            // invoke callback on worker thread
-            mWorkerHandler.post(() -> callback.onFinalResult(metricsConfigName, bundle));
+            callback.onFinalResult(metricsConfigName, bundle);
         } catch (IOException e) {
             Slog.w(CarLog.TAG_TELEMETRY, "Failed to get final result from disk.", e);
-            mWorkerHandler.post(() -> callback.onFinalResult(metricsConfigName, null));
+            callback.onFinalResult(metricsConfigName, null);
         }
         if (deleteResult) {
             file.delete();
@@ -154,40 +125,31 @@
      * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
      */
     public void putFinalResult(String metricsConfigName, PersistableBundle result) {
-        synchronized (mLock) {
-            mIoHandler.post(() -> {
-                writeSingleResultToFileOnIoThread(mFinalResultDirectory, metricsConfigName, result);
-                deleteSingleFileOnIoThread(mInterimResultDirectory, metricsConfigName);
-            });
-            mInterimResultCache.remove(metricsConfigName);
-        }
+        writePersistableBundleToFile(mFinalResultDirectory, metricsConfigName, result);
+        deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
+        mInterimResultCache.remove(metricsConfigName);
     }
 
     /** Persists data to disk. */
     public void flushToDisk() {
-        mIoHandler.post(() -> {
-            synchronized (mLock) {
-                writeInterimResultsToFileLockedOnIoThread();
-                deleteStaleDataOnIoThread(mInterimResultDirectory, mFinalResultDirectory);
-            }
-        });
+        writeInterimResultsToFile();
+        deleteAllStaleData(mInterimResultDirectory, mFinalResultDirectory);
     }
 
     /** Writes dirty interim results to disk. */
-    @GuardedBy("mLock")
-    private void writeInterimResultsToFileLockedOnIoThread() {
+    private void writeInterimResultsToFile() {
         mInterimResultCache.forEach((metricsConfigName, interimResult) -> {
             // only write dirty data
             if (!interimResult.isDirty()) {
                 return;
             }
-            writeSingleResultToFileOnIoThread(
+            writePersistableBundleToFile(
                     mInterimResultDirectory, metricsConfigName, interimResult.getBundle());
         });
     }
 
     /** Deletes data that are older than some threshold in the given directories. */
-    private void deleteStaleDataOnIoThread(File... dirs) {
+    private void deleteAllStaleData(File... dirs) {
         long currTimeMs = System.currentTimeMillis();
         for (File dir : dirs) {
             for (File file : dir.listFiles()) {
@@ -202,13 +164,10 @@
     /**
      * Converts a {@link PersistableBundle} into byte array and saves the results to a file.
      */
-    private void writeSingleResultToFileOnIoThread(
+    private void writePersistableBundleToFile(
             File dir, String metricsConfigName, PersistableBundle result) {
-        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
-            result.writeToStream(outputStream);
-            Files.write(
-                    new File(dir, metricsConfigName).toPath(),
-                    outputStream.toByteArray());
+        try (FileOutputStream os = new FileOutputStream(new File(dir, metricsConfigName))) {
+            result.writeToStream(os);
         } catch (IOException e) {
             Slog.w(CarLog.TAG_TELEMETRY, "Failed to write result to file", e);
             // TODO(b/195422219): record failure
@@ -216,7 +175,7 @@
     }
 
     /** Deletes a the given file in the given directory if it exists. */
-    private void deleteSingleFileOnIoThread(File interimResultDirectory, String metricsConfigName) {
+    private void deleteFileInDirectory(File interimResultDirectory, String metricsConfigName) {
         File file = new File(interimResultDirectory, metricsConfigName);
         file.delete();
     }
diff --git a/service/src/com/android/car/telemetry/databroker/DataBroker.java b/service/src/com/android/car/telemetry/databroker/DataBroker.java
index f22a8ca..d7cc2e6 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBroker.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBroker.java
@@ -55,13 +55,11 @@
     /**
      * Adds a {@link ScriptExecutionTask} to the priority queue. This method will schedule the
      * next task if a task is not currently running.
-     * This method can be called from any thread, and it is thread-safe.
      */
     void addTaskToQueue(ScriptExecutionTask task);
 
     /**
-     * Sends a message to the handler to poll and execute a task.
-     * This method is thread-safe.
+     * Checks system health state and executes a task if condition allows.
      */
     void scheduleNextTask();
 
diff --git a/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java b/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
index 54e45b9..55c7685 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -34,15 +33,14 @@
 
 import com.android.car.CarLog;
 import com.android.car.CarServiceUtils;
-import com.android.car.scriptexecutor.IScriptExecutor;
-import com.android.car.scriptexecutor.IScriptExecutorListener;
 import com.android.car.telemetry.CarTelemetryService;
 import com.android.car.telemetry.ResultStore;
 import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.TelemetryProto.MetricsConfig;
 import com.android.car.telemetry.publisher.AbstractPublisher;
 import com.android.car.telemetry.publisher.PublisherFactory;
-import com.android.internal.annotations.GuardedBy;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.ref.WeakReference;
@@ -50,9 +48,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.PriorityBlockingQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Implementation of the data path component of CarTelemetryService. Forwards the published data
@@ -62,33 +57,19 @@
 public class DataBrokerImpl implements DataBroker {
 
     private static final int MSG_HANDLE_TASK = 1;
-    @VisibleForTesting
-    static final int MSG_BIND_TO_SCRIPT_EXECUTOR = 2;
+    private static final int MSG_BIND_TO_SCRIPT_EXECUTOR = 2;
+
+    private static final String SCRIPT_EXECUTOR_PACKAGE = "com.android.car.scriptexecutor";
+    private static final String SCRIPT_EXECUTOR_CLASS =
+            "com.android.car.scriptexecutor.ScriptExecutor";
 
     private final Context mContext;
     private final PublisherFactory mPublisherFactory;
     private final ResultStore mResultStore;
     private final ScriptExecutorListener mScriptExecutorListener;
-    private final Object mLock = new Object();
-    private final HandlerThread mWorkerThread = CarServiceUtils.getHandlerThread(
+    private final HandlerThread mTelemetryThread = CarServiceUtils.getHandlerThread(
             CarTelemetryService.class.getSimpleName());
-    private final Handler mWorkerHandler = new TaskHandler(mWorkerThread.getLooper());
-
-    /** Thread-safe int to determine which data can be processed. */
-    private final AtomicInteger mPriority = new AtomicInteger(1);
-
-    /**
-     * Name of the script that's currently running. If no script is running, value is null.
-     * A non-null script name indicates a script is running, which means DataBroker should not
-     * make another ScriptExecutor binder call.
-     */
-    private final AtomicReference<String> mCurrentScriptName = new AtomicReference<>(null);
-
-    /**
-     * If something irrecoverable happened, DataBroker should enter into a disabled state to prevent
-     * doing futile work.
-     */
-    private final AtomicBoolean mDisabled = new AtomicBoolean(false);
+    private final Handler mTelemetryHandler = new TaskHandler(mTelemetryThread.getLooper());
 
     /** Thread-safe priority queue for scheduling tasks. */
     private final PriorityBlockingQueue<ScriptExecutionTask> mTaskQueue =
@@ -98,43 +79,66 @@
      * Maps MetricsConfig's name to its subscriptions. This map is useful when removing a
      * MetricsConfig.
      */
-    @GuardedBy("mLock")
     private final Map<String, List<DataSubscriber>> mSubscriptionMap = new ArrayMap<>();
-    private final AtomicReference<IScriptExecutor> mScriptExecutor = new AtomicReference<>();
+
+    /**
+     * If something irrecoverable happened, DataBroker should enter into a disabled state to prevent
+     * doing futile work.
+     */
+    private boolean mDisabled = false;
+
+    /** Priority of current system to determine if a {@link ScriptExecutionTask} can run. */
+    private int mPriority = 1;
+
+    /**
+     * Name of the script that's currently running. If no script is running, value is null.
+     * A non-null script name indicates a script is running, which means DataBroker should not
+     * make another ScriptExecutor binder call.
+     */
+    private String mCurrentScriptName;
+    private IScriptExecutor mScriptExecutor;
+    private ScriptFinishedCallback mScriptFinishedCallback;
+
     private final ServiceConnection mServiceConnection = new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
-            mScriptExecutor.set(IScriptExecutor.Stub.asInterface(service));
-            scheduleNextTask();
+            mTelemetryHandler.post(() -> {
+                mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
+                scheduleNextTask();
+            });
         }
 
         @Override
         public void onServiceDisconnected(ComponentName name) {
-            mScriptExecutor.set(null);
-            cleanupBoundService();
+            // TODO(b/198684473): clean up the state after script executor disconnects
+            mTelemetryHandler.post(() -> {
+                mScriptExecutor = null;
+                unbindScriptExecutor();
+            });
         }
     };
 
-    private ScriptFinishedCallback mScriptFinishedCallback;
-
     public DataBrokerImpl(
             Context context, PublisherFactory publisherFactory, ResultStore resultStore) {
         mContext = context;
         mPublisherFactory = publisherFactory;
         mResultStore = resultStore;
         mScriptExecutorListener = new ScriptExecutorListener(this);
-        bindScriptExecutor();
+        mPublisherFactory.setFailureConsumer(this::onPublisherFailure);
     }
 
-    /** Binds to ScriptExecutor. */
+    private void onPublisherFailure(AbstractPublisher publisher, Throwable error) {
+        // TODO(b/193680465): disable MetricsConfig and log the error
+        Slog.w(CarLog.TAG_TELEMETRY, "publisher failed", error);
+    }
+
     private void bindScriptExecutor() {
-        // do not re-bind if broker is in a disabled state or script executor is nonnull
-        if (mDisabled.get() || mScriptExecutor.get() != null) {
+        // do not re-bind if broker is in a disabled state or if script executor is nonnull
+        if (mDisabled || mScriptExecutor != null) {
             return;
         }
         Intent intent = new Intent();
-        intent.setComponent(new ComponentName("com.android.car.scriptexecutor",
-                "com.android.car.scriptexecutor.ScriptExecutor"));
+        intent.setComponent(new ComponentName(SCRIPT_EXECUTOR_PACKAGE, SCRIPT_EXECUTOR_CLASS));
         boolean success = mContext.bindServiceAsUser(
                 intent,
                 mServiceConnection,
@@ -147,8 +151,13 @@
         }
     }
 
-    /** Unbinds {@link ScriptExecutor} to release the connection. */
+    /**
+     * Unbinds {@link ScriptExecutor} to release the connection. This method should be called from
+     * the telemetry thread.
+     */
     private void unbindScriptExecutor() {
+        // TODO(b/198648763): unbind from script executor when there is no work to do
+        mCurrentScriptName = null;
         try {
             mContext.unbindService(mServiceConnection);
         } catch (IllegalArgumentException e) {
@@ -157,53 +166,26 @@
         }
     }
 
-    /**
-     * Cleans up the state after ScriptExecutor is killed by the system & service disconnects.
-     */
-    private void cleanupBoundService() {
-        // TODO(b/187743369): clean up the state after script executor disconnects
-        unbindScriptExecutor();
-        mCurrentScriptName.set(null);
-    }
-
     /** Enters into a disabled state because something irrecoverable happened. */
     private void disableBroker() {
-        mDisabled.set(true);
+        mDisabled = true;
         // remove all MetricConfigs, disable all publishers, stop receiving data
-        synchronized (mLock) {
-            for (String metricsConfigName : mSubscriptionMap.keySet()) {
-                // if no subscriber, remove key from map
-                if (mSubscriptionMap.get(metricsConfigName).size() == 0) {
-                    mSubscriptionMap.remove(metricsConfigName);
-                } else {
-                    // otherwise get the metrics config from the DataSubscriber and remove it
-                    removeMetricsConfiguration(mSubscriptionMap.get(metricsConfigName).get(0)
-                            .getMetricsConfig());
-                }
+        for (String metricsConfigName : mSubscriptionMap.keySet()) {
+            // get the metrics config from the DataSubscriber and remove the metrics config
+            if (mSubscriptionMap.get(metricsConfigName).size() != 0) {
+                removeMetricsConfiguration(mSubscriptionMap.get(metricsConfigName).get(0)
+                        .getMetricsConfig());
             }
         }
+        mSubscriptionMap.clear();
     }
 
     @Override
     public void addMetricsConfiguration(MetricsConfig metricsConfig) {
-        if (mDisabled.get()) {
-            return;
-        }
         // TODO(b/187743369): pass status back to caller
-        mWorkerHandler.post(() -> addMetricsConfigurationOnHandlerThread(metricsConfig));
-    }
-
-    private void addMetricsConfigurationOnHandlerThread(MetricsConfig metricsConfig) {
-        // this method can only be called from the thread that the handler is running at
-        if (Looper.myLooper() != mWorkerHandler.getLooper()) {
-            throw new RuntimeException(
-                    "addMetricsConfigurationOnHandlerThread is not called from handler thread");
-        }
-        synchronized (mLock) {
-            // if metricsConfig already exists, it should not be added again
-            if (mSubscriptionMap.containsKey(metricsConfig.getName())) {
-                return;
-            }
+        // if broker is disabled or metricsConfig already exists, do nothing
+        if (mDisabled || mSubscriptionMap.containsKey(metricsConfig.getName())) {
+            return;
         }
         // Create the subscribers for this metrics configuration
         List<DataSubscriber> dataSubscribers = new ArrayList<>(
@@ -228,33 +210,17 @@
                 return;
             }
         }
-        synchronized (mLock) {
-            mSubscriptionMap.put(metricsConfig.getName(), dataSubscribers);
-        }
+        mSubscriptionMap.put(metricsConfig.getName(), dataSubscribers);
     }
 
     @Override
     public void removeMetricsConfiguration(MetricsConfig metricsConfig) {
         // TODO(b/187743369): pass status back to caller
-        mWorkerHandler.post(() -> removeMetricsConfigurationOnHandlerThread(metricsConfig));
-    }
-
-    private void removeMetricsConfigurationOnHandlerThread(MetricsConfig metricsConfig) {
-        // this method can only be called from the thread that the handler is running at
-        if (Looper.myLooper() != mWorkerHandler.getLooper()) {
-            throw new RuntimeException(
-                    "removeMetricsConfigurationOnHandlerThread is not called from handler thread");
-        }
-        synchronized (mLock) {
-            if (!mSubscriptionMap.containsKey(metricsConfig.getName())) {
-                return;
-            }
+        if (!mSubscriptionMap.containsKey(metricsConfig.getName())) {
+            return;
         }
         // get the subscriptions associated with this MetricsConfig, remove it from the map
-        List<DataSubscriber> dataSubscribers;
-        synchronized (mLock) {
-            dataSubscribers = mSubscriptionMap.remove(metricsConfig.getName());
-        }
+        List<DataSubscriber> dataSubscribers = mSubscriptionMap.remove(metricsConfig.getName());
         // for each subscriber, remove it from publishers
         for (DataSubscriber subscriber : dataSubscribers) {
             AbstractPublisher publisher = mPublisherFactory.getPublisher(
@@ -276,7 +242,7 @@
 
     @Override
     public void addTaskToQueue(ScriptExecutionTask task) {
-        if (mDisabled.get()) {
+        if (mDisabled) {
             return;
         }
         mTaskQueue.add(task);
@@ -290,20 +256,19 @@
      * the handler handles message in the order they come in, this means the task will be polled
      * sequentially instead of concurrently. Every task that is scheduled and run will be distinct.
      * TODO(b/187743369): If the threading behavior in DataSubscriber changes, ScriptExecutionTask
-     * will also have different threading behavior. Update javadoc when the
-     * behavior is decided.
+     *  will also have different threading behavior. Update javadoc when the behavior is decided.
      */
     @Override
     public void scheduleNextTask() {
-        if (mDisabled.get() || mCurrentScriptName.get() != null || mTaskQueue.peek() == null) {
+        if (mDisabled || mTelemetryHandler.hasMessages(MSG_HANDLE_TASK)) {
             return;
         }
-        mWorkerHandler.sendMessage(mWorkerHandler.obtainMessage(MSG_HANDLE_TASK));
+        mTelemetryHandler.sendEmptyMessage(MSG_HANDLE_TASK);
     }
 
     @Override
     public void setOnScriptFinishedCallback(ScriptFinishedCallback callback) {
-        if (mDisabled.get()) {
+        if (mDisabled) {
             return;
         }
         mScriptFinishedCallback = callback;
@@ -311,23 +276,21 @@
 
     @Override
     public void setTaskExecutionPriority(int priority) {
-        if (mDisabled.get()) {
+        if (mDisabled) {
             return;
         }
-        mPriority.set(priority);
+        mPriority = priority;
         scheduleNextTask(); // when priority updates, schedule a task which checks task queue
     }
 
     @VisibleForTesting
     Map<String, List<DataSubscriber>> getSubscriptionMap() {
-        synchronized (mLock) {
-            return new ArrayMap<>((ArrayMap<String, List<DataSubscriber>>) mSubscriptionMap);
-        }
+        return new ArrayMap<>((ArrayMap<String, List<DataSubscriber>>) mSubscriptionMap);
     }
 
     @VisibleForTesting
-    Handler getWorkerHandler() {
-        return mWorkerHandler;
+    Handler getTelemetryHandler() {
+        return mTelemetryHandler;
     }
 
     @VisibleForTesting
@@ -342,77 +305,65 @@
      * lower priority number to be polled.
      */
     private void pollAndExecuteTask() {
-        // this method can only be called from the thread that the handler is running at
-        if (Looper.myLooper() != mWorkerHandler.getLooper()) {
-            throw new RuntimeException("pollAndExecuteTask is not called from handler thread");
-        }
-        // all checks are thread-safe
-        if (mDisabled.get()
-                || mCurrentScriptName.get() != null
-                || mTaskQueue.peek() == null
-                || mTaskQueue.peek().getPriority() > mPriority.get()) {
+        // check databroker state is ready to run script
+        if (mDisabled || mCurrentScriptName != null) {
             return;
         }
-        IScriptExecutor scriptExecutor = mScriptExecutor.get();
-        if (scriptExecutor == null) {
-            Slog.w(CarLog.TAG_TELEMETRY, "script executor is null, cannot execute task");
-            bindScriptExecutor();
+        // check task is valid and ready to be run
+        ScriptExecutionTask task = mTaskQueue.peek();
+        if (task == null || task.getPriority() > mPriority) {
             return;
         }
-        ScriptExecutionTask task = mTaskQueue.poll();
-        if (task == null) {
-            return;
-        }
-        MetricsConfig metricsConfig = task.getMetricsConfig();
-        mCurrentScriptName.set(metricsConfig.getName()); // signal the start of script execution
+        mTaskQueue.poll(); // remove task from queue
         try {
-            scriptExecutor.invokeScript(
-                    metricsConfig.getScript(),
-                    task.getHandlerName(),
-                    task.getData(),
-                    null, // TODO(b/197027637): PersistableBundle cannot be converted into Bundle
-                    mScriptExecutorListener);
+            if (mScriptExecutor == null) {
+                Slog.w(CarLog.TAG_TELEMETRY,
+                        "script executor is null, cannot execute task");
+                mTaskQueue.add(task);
+                // upon successful binding, a task will be scheduled to run if there are any
+                mTelemetryHandler.sendEmptyMessage(MSG_BIND_TO_SCRIPT_EXECUTOR);
+            } else {
+                // update current name because a script is currently running
+                mCurrentScriptName = task.getMetricsConfig().getName();
+                mScriptExecutor.invokeScript(
+                        task.getMetricsConfig().getScript(),
+                        task.getHandlerName(),
+                        task.getData(),
+                        mResultStore.getInterimResult(mCurrentScriptName),
+                        mScriptExecutorListener);
+            }
         } catch (RemoteException e) {
             Slog.d(CarLog.TAG_TELEMETRY, "remote exception occurred invoking script", e);
             mTaskQueue.add(task); // will not trigger scheduleNextTask()
-            mCurrentScriptName.set(null);
+            mCurrentScriptName = null;
         }
     }
 
     /** Stores final metrics and schedules the next task. */
-    private void onScriptFinished(byte[] result) {
-        // TODO(b/197027637): update API to use PersistableBundle
-        //                    mResultStore.putFinalResult(mCurrentScriptName.get(), result);
-        mCurrentScriptName.set(null);
-        scheduleNextTask();
+    private void onScriptFinished(PersistableBundle result) {
+        mTelemetryHandler.post(() -> {
+            mResultStore.putFinalResult(mCurrentScriptName, result);
+            mCurrentScriptName = null;
+            scheduleNextTask();
+        });
     }
 
     /** Stores interim metrics and schedules the next task. */
-    private void onScriptSuccess(Bundle stateToPersist) {
-        // TODO(b/197027637): update API to use PersistableBundle
-        PersistableBundle persistableBundle = new PersistableBundle();
-        for (String key : stateToPersist.keySet()) {
-            Object value = stateToPersist.get(key);
-            if (value instanceof Integer) {
-                persistableBundle.putInt(key, (int) value);
-            } else if (value instanceof Double) {
-                persistableBundle.putDouble(key, (double) value);
-            } else if (value instanceof Boolean) {
-                persistableBundle.putBoolean(key, (boolean) value);
-            } else if (value instanceof String) {
-                persistableBundle.putString(key, (String) value);
-            }
-        }
-        mResultStore.putInterimResult(mCurrentScriptName.get(), persistableBundle);
-        mCurrentScriptName.set(null);
-        scheduleNextTask();
+    private void onScriptSuccess(PersistableBundle stateToPersist) {
+        mTelemetryHandler.post(() -> {
+            mResultStore.putInterimResult(mCurrentScriptName, stateToPersist);
+            mCurrentScriptName = null;
+            scheduleNextTask();
+        });
     }
 
     /** Stores telemetry error and schedules the next task. */
     private void onScriptError(int errorType, String message, String stackTrace) {
-        // TODO(b/197027637): create error object
-        mCurrentScriptName.set(null);
-        scheduleNextTask();
+        mTelemetryHandler.post(() -> {
+            // TODO(b/197005294): create error object
+            mCurrentScriptName = null;
+            scheduleNextTask();
+        });
     }
 
     /** Listens for script execution status. Methods are called on the binder thread. */
@@ -424,7 +375,7 @@
         }
 
         @Override
-        public void onScriptFinished(byte[] result) {
+        public void onScriptFinished(PersistableBundle result) {
             DataBrokerImpl dataBroker = mWeakDataBroker.get();
             if (dataBroker == null) {
                 return;
@@ -433,7 +384,7 @@
         }
 
         @Override
-        public void onSuccess(Bundle stateToPersist) {
+        public void onSuccess(PersistableBundle stateToPersist) {
             DataBrokerImpl dataBroker = mWeakDataBroker.get();
             if (dataBroker == null) {
                 return;
@@ -470,6 +421,9 @@
                 case MSG_HANDLE_TASK:
                     pollAndExecuteTask(); // run the next task
                     break;
+                case MSG_BIND_TO_SCRIPT_EXECUTOR:
+                    bindScriptExecutor();
+                    break;
                 default:
                     Slog.w(CarLog.TAG_TELEMETRY, "TaskHandler received unknown message.");
             }
diff --git a/service/src/com/android/car/telemetry/databroker/DataSubscriber.java b/service/src/com/android/car/telemetry/databroker/DataSubscriber.java
index 9aa300c..8af2c6f 100644
--- a/service/src/com/android/car/telemetry/databroker/DataSubscriber.java
+++ b/service/src/com/android/car/telemetry/databroker/DataSubscriber.java
@@ -16,19 +16,14 @@
 
 package com.android.car.telemetry.databroker;
 
-import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.os.SystemClock;
 
 import com.android.car.telemetry.TelemetryProto;
 
 /**
  * Subscriber class that receives published data and schedules tasks for execution.
- * The class is thread-safe as long as
- * {@link TelemetryProto.MetricsConfig} does not change during runtime.
- *
- * <p>TODO(b/187743369): thread-safety can change if priority can be updated in runtime. Update
- *                    javadoc once priority is concretely defined.
- *                    Must be thread-safe, as #push() method may be called by multiple threads.
+ * All methods of this class must be accessed on telemetry thread.
  *
  * <p>TODO(b/187743369): implement equals() and hash() functions, as they are used in publishers
  *                       to check equality of subscribers.
@@ -64,13 +59,11 @@
     /**
      * Creates a {@link ScriptExecutionTask} and pushes it to the priority queue where the task
      * will be pending execution.
-     *
-     * <p>This method is thread-safe and doesn't block.
      */
-    public void push(Bundle data) {
+    public void push(PersistableBundle data) {
         ScriptExecutionTask task = new ScriptExecutionTask(
                 this, data, SystemClock.elapsedRealtime());
-        mDataBroker.addTaskToQueue(task); // thread-safe
+        mDataBroker.addTaskToQueue(task);
     }
 
     /** Returns the {@link TelemetryProto.MetricsConfig}. */
diff --git a/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java b/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java
index ae37eae..a1fb522 100644
--- a/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java
+++ b/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java
@@ -16,7 +16,7 @@
 
 package com.android.car.telemetry.databroker;
 
-import android.os.Bundle;
+import android.os.PersistableBundle;
 
 import com.android.car.telemetry.TelemetryProto;
 
@@ -30,9 +30,10 @@
 public class ScriptExecutionTask implements Comparable<ScriptExecutionTask> {
     private final long mTimestampMillis;
     private final DataSubscriber mSubscriber;
-    private final Bundle mData;
+    private final PersistableBundle mData;
 
-    ScriptExecutionTask(DataSubscriber subscriber, Bundle data, long elapsedRealtimeMillis) {
+    ScriptExecutionTask(DataSubscriber subscriber, PersistableBundle data,
+            long elapsedRealtimeMillis) {
         mTimestampMillis = elapsedRealtimeMillis;
         mSubscriber = subscriber;
         mData = data;
@@ -56,7 +57,7 @@
         return mSubscriber.getHandlerName();
     }
 
-    public Bundle getData() {
+    public PersistableBundle getData() {
         return mData;
     }
 
diff --git a/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java b/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
index b5d4d7b..e91831e 100644
--- a/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
+++ b/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
@@ -18,6 +18,8 @@
 
 import com.android.car.telemetry.databroker.DataSubscriber;
 
+import java.util.function.BiConsumer;
+
 /**
  * Abstract class for publishers. It is 1-1 with data source and manages sending data to
  * subscribers. Publisher stops itself when there are no subscribers.
@@ -26,9 +28,17 @@
  * configuration. Single publisher instance can send data as several
  * {@link com.android.car.telemetry.TelemetryProto.Publisher} to subscribers.
  *
- * <p>Child classes must be thread-safe.
+ * <p>Child classes must be called from the telemetry thread.
  */
 public abstract class AbstractPublisher {
+    // TODO(b/199211673): provide list of bad MetricsConfigs to failureConsumer.
+    /** Consumes the publisher failures, such as failing to connect to a underlying service. */
+    private final BiConsumer<AbstractPublisher, Throwable> mFailureConsumer;
+
+    AbstractPublisher(BiConsumer<AbstractPublisher, Throwable> failureConsumer) {
+        mFailureConsumer = failureConsumer;
+    }
+
     /**
      * Adds a subscriber that listens for data produced by this publisher.
      *
@@ -44,7 +54,7 @@
     /**
      * Removes the subscriber from the publisher. Publisher stops if necessary.
      *
-     * @throws IllegalArgumentException if invalid subscriber was provided.
+     * <p>It does nothing if subscriber is not found.
      */
     public abstract void removeDataSubscriber(DataSubscriber subscriber);
 
@@ -57,4 +67,12 @@
 
     /** Returns true if the publisher already has this data subscriber. */
     public abstract boolean hasDataSubscriber(DataSubscriber subscriber);
+
+    /**
+     * Notifies the failure consumer that this publisher cannot recover from the hard failure.
+     * For example, it cannot connect to the underlying service.
+     */
+    protected void notifyFailureConsumer(Throwable error) {
+        mFailureConsumer.accept(this, error);
+    }
 }
diff --git a/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java b/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java
index feeeada..490ab83 100644
--- a/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java
+++ b/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java
@@ -16,10 +16,10 @@
 
 package com.android.car.telemetry.publisher;
 
-import android.annotation.Nullable;
 import android.automotive.telemetry.internal.CarDataInternal;
 import android.automotive.telemetry.internal.ICarDataListener;
 import android.automotive.telemetry.internal.ICarTelemetryInternal;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -29,12 +29,12 @@
 import com.android.car.CarLog;
 import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.databroker.DataSubscriber;
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 import com.android.server.utils.Slogf;
 
 import java.util.ArrayList;
+import java.util.function.BiConsumer;
 
 /**
  * Publisher for cartelemtryd service (aka ICarTelemetry).
@@ -50,57 +50,60 @@
     private static final String SERVICE_NAME = ICarTelemetryInternal.DESCRIPTOR + "/default";
     private static final int BINDER_FLAGS = 0;
 
-    private final Object mLock = new Object();
-
-    @Nullable
-    @GuardedBy("mLock")
     private ICarTelemetryInternal mCarTelemetryInternal;
 
-    @GuardedBy("mLock")
     private final ArrayList<DataSubscriber> mSubscribers = new ArrayList<>();
 
+    // All the methods in this class are expected to be called on this handler's thread.
+    private final Handler mTelemetryHandler;
+
     private final ICarDataListener mListener = new ICarDataListener.Stub() {
         @Override
-        public void onCarDataReceived(CarDataInternal[] dataList) throws RemoteException {
+        public void onCarDataReceived(final CarDataInternal[] dataList) throws RemoteException {
             if (DEBUG) {
                 Slog.d(CarLog.TAG_TELEMETRY,
                         "Received " + dataList.length + " CarData from cartelemetryd");
             }
-            onCarDataListReceived(dataList);
+            // TODO(b/189142577): Create custom Handler and post message to improve performance
+            mTelemetryHandler.post(() -> onCarDataListReceived(dataList));
         }
     };
 
-    /** Called when binder for ICarTelemetry service is died. */
-    void onBinderDied() {
-        synchronized (mLock) {
-            if (mCarTelemetryInternal != null) {
-                mCarTelemetryInternal.asBinder().unlinkToDeath(this::onBinderDied, BINDER_FLAGS);
-            }
-            // TODO(b/193680465): try reconnecting again
-            mCarTelemetryInternal = null;
-        }
+    CarTelemetrydPublisher(BiConsumer<AbstractPublisher, Throwable> failureConsumer,
+            Handler telemetryHandler) {
+        super(failureConsumer);
+        this.mTelemetryHandler = telemetryHandler;
     }
 
-    /**
-     * Connects to ICarTelemetryInternal service and starts listening for CarData.
-     *
-     * @throws IllegalStateException if it cannot connect to ICarTelemetryInternal service.
-     */
-    @GuardedBy("mLock")
-    private void connectToCarTelemetrydLocked() {
+    /** Called when binder for ICarTelemetry service is died. */
+    private void onBinderDied() {
+        // TODO(b/189142577): Create custom Handler and post message to improve performance
+        mTelemetryHandler.post(() -> {
+            if (mCarTelemetryInternal != null) {
+                mCarTelemetryInternal.asBinder().unlinkToDeath(this::onBinderDied, BINDER_FLAGS);
+                mCarTelemetryInternal = null;
+            }
+            notifyFailureConsumer(new IllegalStateException("ICarTelemetryInternal binder died"));
+        });
+    }
+
+    /** Connects to ICarTelemetryInternal service and starts listening for CarData. */
+    private void connectToCarTelemetryd() {
         if (mCarTelemetryInternal != null) {
-            return;  // already connected
+            return;
         }
         IBinder binder = ServiceManager.checkService(SERVICE_NAME);
         if (binder == null) {
-            throw new IllegalStateException(
-                    "Failed to connect to ICarTelemetryInternal: service is not ready");
+            notifyFailureConsumer(new IllegalStateException(
+                    "Failed to connect to the ICarTelemetryInternal: service is not ready"));
+            return;
         }
         try {
             binder.linkToDeath(this::onBinderDied, BINDER_FLAGS);
         } catch (RemoteException e) {
-            throw new IllegalStateException(
-                    "Failed to connect to ICarTelemetryInternal: linkToDeath failed", e);
+            notifyFailureConsumer(new IllegalStateException(
+                    "Failed to connect to the ICarTelemetryInternal: linkToDeath failed", e));
+            return;
         }
         mCarTelemetryInternal = ICarTelemetryInternal.Stub.asInterface(binder);
         try {
@@ -108,9 +111,9 @@
         } catch (RemoteException e) {
             binder.unlinkToDeath(this::onBinderDied, BINDER_FLAGS);
             mCarTelemetryInternal = null;
-            throw new IllegalStateException(
-                    "Failed to connect to ICarTelemetryInternal: Cannot set CarData listener",
-                    e);
+            notifyFailureConsumer(new IllegalStateException(
+                    "Failed to connect to the ICarTelemetryInternal: Cannot set CarData listener",
+                    e));
         }
     }
 
@@ -119,8 +122,7 @@
      *
      * @throws IllegalStateException if fails to clear the listener.
      */
-    @GuardedBy("mLock")
-    private void disconnectFromCarTelemetrydLocked() {
+    private void disconnectFromCarTelemetryd() {
         if (mCarTelemetryInternal == null) {
             return;  // already disconnected
         }
@@ -135,9 +137,7 @@
 
     @VisibleForTesting
     boolean isConnectedToCarTelemetryd() {
-        synchronized (mLock) {
-            return mCarTelemetryInternal != null;
-        }
+        return mCarTelemetryInternal != null;
     }
 
     @Override
@@ -156,46 +156,34 @@
                 "Invalid CarData ID " + carDataId
                         + ". Please see CarData.proto for the list of available IDs.");
 
-        synchronized (mLock) {
-            try {
-                connectToCarTelemetrydLocked();
-            } catch (IllegalStateException e) {
-                Slog.e(CarLog.TAG_TELEMETRY, "Failed to connect to ICarTelemetry", e);
-                // TODO(b/193680465): add retry reconnecting
-            }
-            mSubscribers.add(subscriber);
-        }
+        mSubscribers.add(subscriber);
+
+        connectToCarTelemetryd();
 
         Slogf.d(CarLog.TAG_TELEMETRY, "Subscribing to CarDat.id=%d", carDataId);
     }
 
     @Override
     public void removeDataSubscriber(DataSubscriber subscriber) {
-        synchronized (mLock) {
-            mSubscribers.remove(subscriber);
-            if (mSubscribers.isEmpty()) {
-                disconnectFromCarTelemetrydLocked();
-            }
+        mSubscribers.remove(subscriber);
+        if (mSubscribers.isEmpty()) {
+            disconnectFromCarTelemetryd();
         }
     }
 
     @Override
     public void removeAllDataSubscribers() {
-        synchronized (mLock) {
-            mSubscribers.clear();
-            disconnectFromCarTelemetrydLocked();
-        }
+        mSubscribers.clear();
+        disconnectFromCarTelemetryd();
     }
 
     @Override
     public boolean hasDataSubscriber(DataSubscriber subscriber) {
-        synchronized (mLock) {
-            return mSubscribers.contains(subscriber);
-        }
+        return mSubscribers.contains(subscriber);
     }
 
     /**
-     * Called when publisher receives new car data list. It's executed on a Binder thread.
+     * Called when publisher receives new car data list. It's executed on the telemetry thread.
      */
     private void onCarDataListReceived(CarDataInternal[] dataList) {
         // TODO(b/189142577): implement
diff --git a/service/src/com/android/car/telemetry/publisher/PublisherFactory.java b/service/src/com/android/car/telemetry/publisher/PublisherFactory.java
index 8d0975a..1009d25 100644
--- a/service/src/com/android/car/telemetry/publisher/PublisherFactory.java
+++ b/service/src/com/android/car/telemetry/publisher/PublisherFactory.java
@@ -16,39 +16,82 @@
 
 package com.android.car.telemetry.publisher;
 
+import android.content.SharedPreferences;
+import android.os.Handler;
+
 import com.android.car.CarPropertyService;
 import com.android.car.telemetry.TelemetryProto;
 
+import java.util.function.BiConsumer;
+
 /**
- * Factory class for Publishers. It's expected to have a single factory instance.
+ * Lazy factory class for Publishers. It's expected to have a single factory instance.
+ * Must be called from the telemetry thread.
+ *
+ * <p>It doesn't instantiate all the publishers right away, as in some cases some publishers are
+ * not needed.
  *
  * <p>Thread-safe.
  */
 public class PublisherFactory {
     private final Object mLock = new Object();
     private final CarPropertyService mCarPropertyService;
+    private final Handler mTelemetryHandler;
+    private final StatsManagerProxy mStatsManager;
+    private final SharedPreferences mSharedPreferences;
     private VehiclePropertyPublisher mVehiclePropertyPublisher;
+    private CarTelemetrydPublisher mCarTelemetrydPublisher;
+    private StatsPublisher mStatsPublisher;
 
-    public PublisherFactory(CarPropertyService carPropertyService) {
+    private BiConsumer<AbstractPublisher, Throwable> mFailureConsumer;
+
+    public PublisherFactory(
+            CarPropertyService carPropertyService,
+            Handler handler,
+            StatsManagerProxy statsManager,
+            SharedPreferences sharedPreferences) {
         mCarPropertyService = carPropertyService;
+        mTelemetryHandler = handler;
+        mStatsManager = statsManager;
+        mSharedPreferences = sharedPreferences;
     }
 
-    /** Returns publisher by given type. */
-    public AbstractPublisher getPublisher(
-            TelemetryProto.Publisher.PublisherCase type) {
+    /** Returns the publisher by given type. */
+    public AbstractPublisher getPublisher(TelemetryProto.Publisher.PublisherCase type) {
         // No need to optimize locks, as this method is infrequently called.
         synchronized (mLock) {
             switch (type.getNumber()) {
                 case TelemetryProto.Publisher.VEHICLE_PROPERTY_FIELD_NUMBER:
                     if (mVehiclePropertyPublisher == null) {
                         mVehiclePropertyPublisher = new VehiclePropertyPublisher(
-                                mCarPropertyService);
+                                mCarPropertyService, mFailureConsumer, mTelemetryHandler);
                     }
                     return mVehiclePropertyPublisher;
+                case TelemetryProto.Publisher.CARTELEMETRYD_FIELD_NUMBER:
+                    if (mCarTelemetrydPublisher == null) {
+                        mCarTelemetrydPublisher = new CarTelemetrydPublisher(
+                                mFailureConsumer, mTelemetryHandler);
+                    }
+                    return mCarTelemetrydPublisher;
+                case TelemetryProto.Publisher.STATS_FIELD_NUMBER:
+                    if (mStatsPublisher == null) {
+                        mStatsPublisher = new StatsPublisher(
+                                mFailureConsumer, mStatsManager, mSharedPreferences);
+                    }
+                    return mStatsPublisher;
                 default:
                     throw new IllegalArgumentException(
                             "Publisher type " + type + " is not supported");
             }
         }
     }
+
+    /**
+     * Sets the publisher failure consumer for all the publishers. This is expected to be called
+     * before {@link #getPublisher} method. This is not the best approach, but it suits for this
+     * case.
+     */
+    public void setFailureConsumer(BiConsumer<AbstractPublisher, Throwable> consumer) {
+        mFailureConsumer = consumer;
+    }
 }
diff --git a/service/src/com/android/car/telemetry/publisher/StatsPublisher.java b/service/src/com/android/car/telemetry/publisher/StatsPublisher.java
index 8ab9a50..8d5f7ea 100644
--- a/service/src/com/android/car/telemetry/publisher/StatsPublisher.java
+++ b/service/src/com/android/car/telemetry/publisher/StatsPublisher.java
@@ -18,9 +18,14 @@
 
 import android.app.StatsManager.StatsUnavailableException;
 import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.util.LongSparseArray;
 import android.util.Slog;
 
 import com.android.car.CarLog;
+import com.android.car.telemetry.StatsLogProto;
 import com.android.car.telemetry.StatsdConfigProto;
 import com.android.car.telemetry.StatsdConfigProto.StatsdConfig;
 import com.android.car.telemetry.TelemetryProto;
@@ -30,35 +35,76 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+
 /**
  * Publisher for {@link TelemetryProto.StatsPublisher}.
  *
  * <p>The publisher adds subscriber configurations in StatsD and they persist between reboots and
- * CarTelemetryService restarts. Please use {@link #removeAllDataSubscribers} or
- * {@link #removeDataSubscriber} to clean-up these configs from StatsD store.
- *
- * <p>Thread-safe.
+ * CarTelemetryService restarts. Please use {@link #removeAllDataSubscribers} to clean-up these
+ * configs from StatsD store.
  */
 public class StatsPublisher extends AbstractPublisher {
     // These IDs are used in StatsdConfig and ConfigMetricsReport.
-    private static final int APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID = 1;
-    private static final int APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID = 2;
+    @VisibleForTesting
+    static final int APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID = 1;
+    @VisibleForTesting
+    static final int APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID = 2;
 
     // The atom ids are from frameworks/proto_logging/stats/atoms.proto
-    private static final int ATOM_APP_START_MEMORY_STATE_CAPTURED_ID = 55;
+    @VisibleForTesting
+    static final int ATOM_APP_START_MEMORY_STATE_CAPTURED_ID = 55;
+
+    private static final Duration PULL_REPORTS_PERIOD = Duration.ofMinutes(10);
 
     private static final String SHARED_PREF_CONFIG_KEY_PREFIX = "statsd-publisher-config-id-";
     private static final String SHARED_PREF_CONFIG_VERSION_PREFIX =
             "statsd-publisher-config-version-";
 
+    // TODO(b/197766340): remove unnecessary lock
     private final Object mLock = new Object();
 
     private final StatsManagerProxy mStatsManager;
     private final SharedPreferences mSharedPreferences;
+    private final Handler mTelemetryHandler;
 
-    StatsPublisher(StatsManagerProxy statsManager, SharedPreferences sharedPreferences) {
+    // True if the publisher is periodically pulling reports from StatsD.
+    private final AtomicBoolean mIsPullingReports = new AtomicBoolean(false);
+
+    /** Assign the method to {@link Runnable}, otherwise the handler fails to remove it. */
+    private final Runnable mPullReportsPeriodically = this::pullReportsPeriodically;
+
+    // LongSparseArray is memory optimized, but they can be bit slower for more
+    // than 100 items. We're expecting much less number of subscribers, so these data structures
+    // are ok.
+    // Maps config_key to the set of DataSubscriber.
+    @GuardedBy("mLock")
+    private final LongSparseArray<DataSubscriber> mConfigKeyToSubscribers = new LongSparseArray<>();
+
+    // TODO(b/198331078): Use telemetry thread
+    StatsPublisher(
+            BiConsumer<AbstractPublisher, Throwable> failureConsumer,
+            StatsManagerProxy statsManager,
+            SharedPreferences sharedPreferences) {
+        this(failureConsumer, statsManager, sharedPreferences, new Handler(Looper.myLooper()));
+    }
+
+    @VisibleForTesting
+    StatsPublisher(
+            BiConsumer<AbstractPublisher, Throwable> failureConsumer,
+            StatsManagerProxy statsManager,
+            SharedPreferences sharedPreferences,
+            Handler handler) {
+        super(failureConsumer);
         mStatsManager = statsManager;
         mSharedPreferences = sharedPreferences;
+        mTelemetryHandler = handler;
     }
 
     @Override
@@ -69,8 +115,60 @@
                 "Subscribers only with StatsPublisher are supported by this class.");
 
         synchronized (mLock) {
-            addStatsConfigLocked(subscriber);
+            long configKey = addStatsConfigLocked(subscriber);
+            mConfigKeyToSubscribers.put(configKey, subscriber);
         }
+
+        if (!mIsPullingReports.getAndSet(true)) {
+            mTelemetryHandler.postDelayed(mPullReportsPeriodically, PULL_REPORTS_PERIOD.toMillis());
+        }
+    }
+
+    private void processReport(long configKey, StatsLogProto.ConfigMetricsReportList report) {
+        // TODO(b/197269115): parse the report
+        Slog.i(CarLog.TAG_TELEMETRY, "Received reports: " + report.getReportsCount());
+        if (report.getReportsCount() > 0) {
+            PersistableBundle data = new PersistableBundle();
+            // TODO(b/197269115): parse the report
+            data.putInt("reportsCount", report.getReportsCount());
+            DataSubscriber subscriber = getSubscriberByConfigKey(configKey);
+            if (subscriber != null) {
+                subscriber.push(data);
+            }
+        }
+    }
+
+    private void pullReportsPeriodically() {
+        for (long configKey : getActiveConfigKeys()) {
+            try {
+                processReport(configKey, StatsLogProto.ConfigMetricsReportList.parseFrom(
+                        mStatsManager.getReports(configKey)));
+            } catch (StatsUnavailableException e) {
+                // If the StatsD is not available, retry in the next pullReportsPeriodically call.
+                break;
+            } catch (InvalidProtocolBufferException e) {
+                // This case should never happen.
+                Slog.w(CarLog.TAG_TELEMETRY,
+                        "Failed to parse report from statsd, configKey=" + configKey);
+            }
+        }
+
+        if (mIsPullingReports.get()) {
+            mTelemetryHandler.postDelayed(mPullReportsPeriodically, PULL_REPORTS_PERIOD.toMillis());
+        }
+    }
+
+    private List<Long> getActiveConfigKeys() {
+        ArrayList<Long> result = new ArrayList<>();
+        synchronized (mLock) {
+            mSharedPreferences.getAll().forEach((key, value) -> {
+                if (!key.startsWith(SHARED_PREF_CONFIG_KEY_PREFIX)) {
+                    return;
+                }
+                result.add((long) value);
+            });
+        }
+        return result;
     }
 
     /**
@@ -81,19 +179,88 @@
      */
     @Override
     public void removeDataSubscriber(DataSubscriber subscriber) {
-        // TODO(b/189143813): implement
+        TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+        if (publisherParam.getPublisherCase() != PublisherCase.STATS) {
+            Slog.w(CarLog.TAG_TELEMETRY,
+                    "Expected STATS publisher, but received "
+                            + publisherParam.getPublisherCase().name());
+            return;
+        }
+        synchronized (mLock) {
+            long configKey = removeStatsConfigLocked(subscriber);
+            mConfigKeyToSubscribers.remove(configKey);
+        }
+
+        if (mConfigKeyToSubscribers.size() == 0) {
+            mIsPullingReports.set(false);
+            mTelemetryHandler.removeCallbacks(mPullReportsPeriodically);
+        }
     }
 
     /** Removes all the subscribers from the publisher removes StatsdConfigs from StatsD service. */
     @Override
     public void removeAllDataSubscribers() {
-        // TODO(b/189143813): implement
+        synchronized (mLock) {
+            SharedPreferences.Editor editor = mSharedPreferences.edit();
+            mSharedPreferences.getAll().forEach((key, value) -> {
+                if (!key.startsWith(SHARED_PREF_CONFIG_KEY_PREFIX)) {
+                    return;
+                }
+                long configKey = (long) value;
+                try {
+                    mStatsManager.removeConfig(configKey);
+                    String sharedPrefVersion = buildSharedPrefConfigVersionKey(configKey);
+                    editor.remove(key).remove(sharedPrefVersion);
+                } catch (StatsUnavailableException e) {
+                    Slog.w(CarLog.TAG_TELEMETRY, "Failed to remove config " + configKey
+                            + ". Ignoring the failure. Will retry removing again when"
+                            + " removeAllDataSubscribers() is called.", e);
+                    // If it cannot remove statsd config, it's less likely it can delete it even if
+                    // retry. So we will just ignore the failures. The next call of this method
+                    // will ry deleting StatsD configs again.
+                }
+            });
+            editor.apply();
+            mConfigKeyToSubscribers.clear();
+        }
+        mIsPullingReports.set(false);
+        mTelemetryHandler.removeCallbacks(mPullReportsPeriodically);
     }
 
     @Override
     public boolean hasDataSubscriber(DataSubscriber subscriber) {
-        // TODO(b/189143813): implement
-        return true;
+        TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+        if (publisherParam.getPublisherCase() != PublisherCase.STATS) {
+            return false;
+        }
+        long configKey = buildConfigKey(subscriber);
+        synchronized (mLock) {
+            return mConfigKeyToSubscribers.indexOfKey(configKey) >= 0;
+        }
+    }
+
+    /** Returns a subscriber for the given statsd config key. Returns null if not found. */
+    private DataSubscriber getSubscriberByConfigKey(long configKey) {
+        synchronized (mLock) {
+            return mConfigKeyToSubscribers.get(configKey);
+        }
+    }
+
+    /**
+     * Returns the key for SharedPreferences to store/retrieve configKey associated with the
+     * subscriber.
+     */
+    private static String buildSharedPrefConfigKey(DataSubscriber subscriber) {
+        return SHARED_PREF_CONFIG_KEY_PREFIX + subscriber.getMetricsConfig().getName() + "-"
+                + subscriber.getSubscriber().getHandler();
+    }
+
+    /**
+     * Returns the key for SharedPreferences to store/retrieve {@link TelemetryProto.MetricsConfig}
+     * version associated with the configKey (which is generated per DataSubscriber).
+     */
+    private static String buildSharedPrefConfigVersionKey(long configKey) {
+        return SHARED_PREF_CONFIG_VERSION_PREFIX + configKey;
     }
 
     /**
@@ -102,26 +269,19 @@
      * the MetricsConfig (of CarTelemetryService) has a new version.
      */
     @GuardedBy("mLock")
-    private void addStatsConfigLocked(DataSubscriber subscriber) {
-        String sharedPrefConfigKey =
-                SHARED_PREF_CONFIG_KEY_PREFIX + subscriber.getMetricsConfig().getName() + "-"
-                        + subscriber.getSubscriber().getHandler();
-        // Store MetricsConfig (of CarTelemetryService) version per handler_function.
-        String sharedPrefVersion =
-                SHARED_PREF_CONFIG_VERSION_PREFIX + subscriber.getMetricsConfig().getName() + "-"
-                        + subscriber.getSubscriber().getHandler();
+    private long addStatsConfigLocked(DataSubscriber subscriber) {
         long configKey = buildConfigKey(subscriber);
-        StatsdConfig config = buildStatsdConfig(subscriber, configKey);
+        // Store MetricsConfig (of CarTelemetryService) version per handler_function.
+        String sharedPrefVersion = buildSharedPrefConfigVersionKey(configKey);
         if (mSharedPreferences.contains(sharedPrefVersion)) {
             int currentVersion = mSharedPreferences.getInt(sharedPrefVersion, 0);
-            if (currentVersion < subscriber.getMetricsConfig().getVersion()) {
-                // TODO(b/189143813): remove old version from StatsD
-                Slog.d(CarLog.TAG_TELEMETRY, "Removing old config from StatsD");
-            } else {
-                // Ignore if the MetricsConfig version is current or older.
-                return;
-            }
+            if (currentVersion >= subscriber.getMetricsConfig().getVersion()) {
+                // It's trying to add current or older MetricsConfig version, just ignore it.
+                return configKey;
+            }  // if the subscriber's MetricsConfig version is newer, it will replace the old one.
         }
+        String sharedPrefConfigKey = buildSharedPrefConfigKey(subscriber);
+        StatsdConfig config = buildStatsdConfig(subscriber, configKey);
         try {
             // It doesn't throw exception if the StatsdConfig is invalid. But it shouldn't happen,
             // as we generate well-tested StatsdConfig in this service.
@@ -131,10 +291,35 @@
                     .putLong(sharedPrefConfigKey, configKey)
                     .apply();
         } catch (StatsUnavailableException e) {
-            Slog.w(CarLog.TAG_TELEMETRY, "Failed to add config", e);
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to add config" + configKey, e);
             // TODO(b/189143813): if StatsManager is not ready, retry N times and hard fail after
             //                    by notifying DataBroker.
+            // We will notify the failure immediately, as we're expecting StatsManager to be stable.
+            notifyFailureConsumer(
+                    new IllegalStateException("Failed to add config " + configKey, e));
         }
+        return configKey;
+    }
+
+    /** Removes StatsdConfig and returns configKey. */
+    @GuardedBy("mLock")
+    private long removeStatsConfigLocked(DataSubscriber subscriber) {
+        String sharedPrefConfigKey = buildSharedPrefConfigKey(subscriber);
+        long configKey = buildConfigKey(subscriber);
+        // Store MetricsConfig (of CarTelemetryService) version per handler_function.
+        String sharedPrefVersion = buildSharedPrefConfigVersionKey(configKey);
+        try {
+            mStatsManager.removeConfig(configKey);
+            mSharedPreferences.edit().remove(sharedPrefVersion).remove(sharedPrefConfigKey).apply();
+        } catch (StatsUnavailableException e) {
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to remove config " + configKey
+                    + ". Ignoring the failure. Will retry removing again when"
+                    + " removeAllDataSubscribers() is called.", e);
+            // If it cannot remove statsd config, it's less likely it can delete it even if we
+            // retry. So we will just ignore the failures. The next call of this method will
+            // try deleting StatsD configs again.
+        }
+        return configKey;
     }
 
     /**
diff --git a/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java b/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
index 146d995..dbfc002 100644
--- a/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
+++ b/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
@@ -20,7 +20,8 @@
 import android.car.hardware.CarPropertyConfig;
 import android.car.hardware.property.CarPropertyEvent;
 import android.car.hardware.property.ICarPropertyEventListener;
-import android.os.Bundle;
+import android.os.Handler;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.util.ArraySet;
 import android.util.Slog;
@@ -31,18 +32,16 @@
 import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.TelemetryProto.Publisher.PublisherCase;
 import com.android.car.telemetry.databroker.DataSubscriber;
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
 import java.util.List;
+import java.util.function.BiConsumer;
 
 /**
  * Publisher for Vehicle Property changes, aka {@code CarPropertyService}.
  *
  * <p> When a subscriber is added, it registers a car property change listener for the
  * property id of the subscriber and starts pushing the change events to the subscriber.
- *
- * <p>Thread-safe.
  */
 public class VehiclePropertyPublisher extends AbstractPublisher {
     private static final boolean DEBUG = false;  // STOPSHIP if true
@@ -50,17 +49,16 @@
     /** Bundle key for {@link CarPropertyEvent}. */
     public static final String CAR_PROPERTY_EVENT_KEY = "car_property_event";
 
-    // Used to synchronize add/remove DataSubscriber and CarPropertyEvent listener calls.
-    private final Object mLock = new Object();
-
     private final CarPropertyService mCarPropertyService;
+    private final Handler mTelemetryHandler;
 
     // The class only reads, no need to synchronize this object.
+    // Maps property_id to CarPropertyConfig.
     private final SparseArray<CarPropertyConfig> mCarPropertyList;
 
     // SparseArray and ArraySet are memory optimized, but they can be bit slower for more
     // than 100 items. We're expecting much less number of subscribers, so these DS are ok.
-    @GuardedBy("mLock")
+    // Maps property_id to the set of DataSubscriber.
     private final SparseArray<ArraySet<DataSubscriber>> mCarPropertyToSubscribers =
             new SparseArray<>();
 
@@ -78,8 +76,11 @@
                 }
             };
 
-    public VehiclePropertyPublisher(CarPropertyService carPropertyService) {
+    public VehiclePropertyPublisher(CarPropertyService carPropertyService,
+            BiConsumer<AbstractPublisher, Throwable> failureConsumer, Handler handler) {
+        super(failureConsumer);
         mCarPropertyService = carPropertyService;
+        mTelemetryHandler = handler;
         // Load car property list once, as the list doesn't change runtime.
         List<CarPropertyConfig> propertyList = mCarPropertyService.getPropertyList();
         mCarPropertyList = new SparseArray<>(propertyList.size());
@@ -106,19 +107,17 @@
                         == CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ_WRITE,
                 "No access. Cannot read " + VehiclePropertyIds.toString(propertyId) + ".");
 
-        synchronized (mLock) {
-            ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
-            if (subscribers == null) {
-                subscribers = new ArraySet<>();
-                mCarPropertyToSubscribers.put(propertyId, subscribers);
-                // Register the listener only once per propertyId.
-                mCarPropertyService.registerListener(
-                        propertyId,
-                        publisherParam.getVehicleProperty().getReadRate(),
-                        mCarPropertyEventListener);
-            }
-            subscribers.add(subscriber);
+        ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
+        if (subscribers == null) {
+            subscribers = new ArraySet<>();
+            mCarPropertyToSubscribers.put(propertyId, subscribers);
+            // Register the listener only once per propertyId.
+            mCarPropertyService.registerListener(
+                    propertyId,
+                    publisherParam.getVehicleProperty().getReadRate(),
+                    mCarPropertyEventListener);
         }
+        subscribers.add(subscriber);
     }
 
     @Override
@@ -132,32 +131,28 @@
         }
         int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
 
-        synchronized (mLock) {
-            ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
-            if (subscribers == null) {
-                return;
-            }
-            subscribers.remove(subscriber);
-            if (subscribers.isEmpty()) {
-                mCarPropertyToSubscribers.remove(propertyId);
-                // Doesn't throw exception as listener is not null. mCarPropertyService and
-                // local mCarPropertyToSubscribers will not get out of sync.
-                mCarPropertyService.unregisterListener(propertyId, mCarPropertyEventListener);
-            }
+        ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
+        if (subscribers == null) {
+            return;
+        }
+        subscribers.remove(subscriber);
+        if (subscribers.isEmpty()) {
+            mCarPropertyToSubscribers.remove(propertyId);
+            // Doesn't throw exception as listener is not null. mCarPropertyService and
+            // local mCarPropertyToSubscribers will not get out of sync.
+            mCarPropertyService.unregisterListener(propertyId, mCarPropertyEventListener);
         }
     }
 
     @Override
     public void removeAllDataSubscribers() {
-        synchronized (mLock) {
-            for (int i = 0; i < mCarPropertyToSubscribers.size(); i++) {
-                int propertyId = mCarPropertyToSubscribers.keyAt(i);
-                // Doesn't throw exception as listener is not null. mCarPropertyService and
-                // local mCarPropertyToSubscribers will not get out of sync.
-                mCarPropertyService.unregisterListener(propertyId, mCarPropertyEventListener);
-            }
-            mCarPropertyToSubscribers.clear();
+        for (int i = 0; i < mCarPropertyToSubscribers.size(); i++) {
+            int propertyId = mCarPropertyToSubscribers.keyAt(i);
+            // Doesn't throw exception as listener is not null. mCarPropertyService and
+            // local mCarPropertyToSubscribers will not get out of sync.
+            mCarPropertyService.unregisterListener(propertyId, mCarPropertyEventListener);
         }
+        mCarPropertyToSubscribers.clear();
     }
 
     @Override
@@ -167,11 +162,8 @@
             return false;
         }
         int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
-
-        synchronized (mLock) {
-            ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
-            return subscribers != null && subscribers.contains(subscriber);
-        }
+        ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
+        return subscribers != null && subscribers.contains(subscriber);
     }
 
     /**
@@ -179,19 +171,15 @@
      * worker thread.
      */
     private void onVehicleEvent(CarPropertyEvent event) {
-        Bundle bundle = new Bundle();
-        bundle.putParcelable(CAR_PROPERTY_EVENT_KEY, event);
-        ArraySet<DataSubscriber> subscribersClone;
-
-        synchronized (mLock) {
-            subscribersClone = new ArraySet<>(
+        // move the work from CarPropertyService's worker thread to the telemetry thread
+        mTelemetryHandler.post(() -> {
+            // TODO(b/197269115): convert CarPropertyEvent into PersistableBundle
+            PersistableBundle bundle = new PersistableBundle();
+            ArraySet<DataSubscriber> subscribersClone = new ArraySet<>(
                     mCarPropertyToSubscribers.get(event.getCarPropertyValue().getPropertyId()));
-        }
-
-        // Call external methods outside of mLock. If the external method invokes this class's
-        // methods again, it will cause a deadlock.
-        for (DataSubscriber subscriber : subscribersClone) {
-            subscriber.push(bundle);
-        }
+            for (DataSubscriber subscriber : subscribersClone) {
+                subscriber.push(bundle);
+            }
+        });
     }
 }
diff --git a/packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutor.aidl b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl
similarity index 82%
rename from packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutor.aidl
rename to service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl
index a18bedb..c768161 100644
--- a/packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutor.aidl
+++ b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl
@@ -14,16 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.car.scriptexecutor;
+package com.android.car.telemetry.scriptexecutorinterface;
 
-import com.android.car.scriptexecutor.IScriptExecutorListener;
-import android.os.Bundle;
+import android.os.PersistableBundle;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
 
 /**
  * An internal API provided by isolated Script Executor process
  * for executing Lua scripts in a sandboxed environment
- *
- * @hide
  */
 interface IScriptExecutor {
   /**
@@ -37,7 +35,7 @@
    */
   void invokeScript(String scriptBody,
                     String functionName,
-                    in Bundle publishedData,
-                    in @nullable Bundle savedState,
+                    in PersistableBundle publishedData,
+                    in @nullable PersistableBundle savedState,
                     in IScriptExecutorListener listener);
 }
diff --git a/packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutorConstants.aidl b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.aidl
similarity index 95%
rename from packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutorConstants.aidl
rename to service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.aidl
index 3d969b0..52f4cbe 100644
--- a/packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutorConstants.aidl
+++ b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.aidl
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.car.scriptexecutor;
+package com.android.car.telemetry.scriptexecutorinterface;
 
 // TODO(b/194324369): Investigate if we could combine it
 // with IScriptExecutorListener.aidl
diff --git a/packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutorListener.aidl b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
similarity index 89%
rename from packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutorListener.aidl
rename to service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
index 8295f5a..dc64732 100644
--- a/packages/ScriptExecutor/interface/com/android/car/scriptexecutor/IScriptExecutorListener.aidl
+++ b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.car.scriptexecutor;
+package com.android.car.telemetry.scriptexecutorinterface;
 
-import android.os.Bundle;
+import android.os.PersistableBundle;
 
 /**
  * Listener for {@code IScriptExecutor#invokeScript}.
@@ -33,7 +33,7 @@
    *
    * @param result final results of the script that will be uploaded.
    */
-  void onScriptFinished(in byte[] result);
+  void onScriptFinished(in PersistableBundle result);
 
   /**
    * Called by ScriptExecutor when a function completes successfully and also provides
@@ -41,7 +41,7 @@
    *
    * @param stateToPersist key-value pairs to persist
    */
-  void onSuccess(in @nullable Bundle stateToPersist);
+  void onSuccess(in @nullable PersistableBundle stateToPersist);
 
   /**
    * Called by ScriptExecutor to report errors that prevented the script
diff --git a/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java b/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
index e9b1cb4..0dc5563 100644
--- a/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
+++ b/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
@@ -24,7 +24,6 @@
 import android.util.Slog;
 
 import com.android.car.CarLog;
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.BufferedReader;
@@ -34,6 +33,7 @@
 /**
  * SystemMonitor monitors system states and report to listeners when there are
  * important changes.
+ * All methods in this class should be invoked from the telemetry thread.
  */
 public class SystemMonitor {
 
@@ -46,20 +46,16 @@
 
     private static final int POLL_INTERVAL_MILLIS = 60000;
 
-    private final Handler mWorkerHandler;
-
-    private final Object mLock = new Object();
+    private final Handler mTelemetryHandler;
 
     private final Context mContext;
     private final ActivityManager mActivityManager;
     private final String mLoadavgPath;
+    private final Runnable mSystemLoadRunnable = this::getSystemLoadRepeated;
 
-    @GuardedBy("mLock")
     @Nullable private SystemMonitorCallback mCallback;
-    @GuardedBy("mLock")
     private boolean mSystemMonitorRunning = false;
 
-
     /**
      * Interface for receiving notifications about system monitor changes.
      */
@@ -84,9 +80,9 @@
     }
 
     @VisibleForTesting
-    SystemMonitor(Context context, Handler workerHandler, String loadavgPath) {
+    SystemMonitor(Context context, Handler telemetryHandler, String loadavgPath) {
         mContext = context;
-        mWorkerHandler = workerHandler;
+        mTelemetryHandler = telemetryHandler;
         mActivityManager = (ActivityManager)
                 mContext.getSystemService(Context.ACTIVITY_SERVICE);
         mLoadavgPath = loadavgPath;
@@ -98,11 +94,9 @@
      * @param callback the callback to nofify state changes on.
      */
     public void setSystemMonitorCallback(SystemMonitorCallback callback) {
-        synchronized (mLock) {
-            mCallback = callback;
-            if (!mWorkerHandler.hasCallbacks(this::getSystemLoadRepeated)) {
-                startSystemLoadMonitoring();
-            }
+        mCallback = callback;
+        if (!mSystemMonitorRunning) {
+            startSystemLoadMonitoring();
         }
     }
 
@@ -110,10 +104,9 @@
      * Unsets the {@link SystemMonitorCallback}.
      */
     public void unsetSystemMonitorCallback() {
-        synchronized (mLock) {
-            stopSystemLoadMonitoringLocked();
-            mCallback = null;
-        }
+        mTelemetryHandler.removeCallbacks(mSystemLoadRunnable);
+        mSystemMonitorRunning = false;
+        mCallback = null;
     }
 
     /**
@@ -200,25 +193,23 @@
      * The Runnable to repeatedly getting system load data with some interval.
      */
     private void getSystemLoadRepeated() {
-        synchronized (mLock) {
-            try {
-                CpuLoadavg cpuLoadAvg = getCpuLoad();
-                if (cpuLoadAvg == null) {
-                    return;
-                }
-                int numProcessors = Runtime.getRuntime().availableProcessors();
+        try {
+            CpuLoadavg cpuLoadAvg = getCpuLoad();
+            if (cpuLoadAvg == null) {
+                return;
+            }
+            int numProcessors = Runtime.getRuntime().availableProcessors();
 
-                MemoryInfo memInfo = getMemoryLoad();
+            MemoryInfo memInfo = getMemoryLoad();
 
-                SystemMonitorEvent event = new SystemMonitorEvent();
-                setEventCpuUsageLevel(event, cpuLoadAvg.mOneMinuteVal / numProcessors);
-                setEventMemUsageLevel(event, 1 - memInfo.availMem / memInfo.totalMem);
+            SystemMonitorEvent event = new SystemMonitorEvent();
+            setEventCpuUsageLevel(event, cpuLoadAvg.mOneMinuteVal / numProcessors);
+            setEventMemUsageLevel(event, 1 - memInfo.availMem / memInfo.totalMem);
 
-                mCallback.onSystemMonitorEvent(event);
-            } finally {
-                if (mSystemMonitorRunning) {
-                    mWorkerHandler.postDelayed(this::getSystemLoadRepeated, POLL_INTERVAL_MILLIS);
-                }
+            mCallback.onSystemMonitorEvent(event);
+        } finally {
+            if (mSystemMonitorRunning) {
+                mTelemetryHandler.postDelayed(mSystemLoadRunnable, POLL_INTERVAL_MILLIS);
             }
         }
     }
@@ -227,21 +218,8 @@
      * Starts system load monitoring.
      */
     private void startSystemLoadMonitoring() {
-        synchronized (mLock) {
-            mWorkerHandler.post(this::getSystemLoadRepeated);
-            mSystemMonitorRunning = true;
-        }
-    }
-
-    /**
-     * Stops system load monitoring.
-     */
-    @GuardedBy("mLock")
-    private void stopSystemLoadMonitoringLocked() {
-        synchronized (mLock) {
-            mWorkerHandler.removeCallbacks(this::getSystemLoadRepeated);
-            mSystemMonitorRunning = false;
-        }
+        mTelemetryHandler.post(mSystemLoadRunnable);
+        mSystemMonitorRunning = true;
     }
 
     static final class CpuLoadavg {
diff --git a/service/src/com/android/car/watchdog/CarWatchdogService.java b/service/src/com/android/car/watchdog/CarWatchdogService.java
index 5ecefcf..93243d4 100644
--- a/service/src/com/android/car/watchdog/CarWatchdogService.java
+++ b/service/src/com/android/car/watchdog/CarWatchdogService.java
@@ -64,6 +64,7 @@
 import com.android.server.utils.Slogf;
 
 import java.lang.ref.WeakReference;
+import java.time.Instant;
 import java.util.List;
 
 /**
@@ -76,10 +77,22 @@
             "com.android.server.jobscheduler.GARAGE_MODE_ON";
     static final String ACTION_GARAGE_MODE_OFF =
             "com.android.server.jobscheduler.GARAGE_MODE_OFF";
+    static final TimeSourceInterface SYSTEM_INSTANCE = new TimeSourceInterface() {
+        @Override
+        public Instant now() {
+            return Instant.now();
+        }
+
+        @Override
+        public String toString() {
+            return "System time instance";
+        }
+    };
 
     private final Context mContext;
     private final ICarWatchdogServiceForSystemImpl mWatchdogServiceForSystem;
     private final PackageInfoHandler mPackageInfoHandler;
+    private final WatchdogStorage mWatchdogStorage;
     private final WatchdogProcessHandler mWatchdogProcessHandler;
     private final WatchdogPerfHandler mWatchdogPerfHandler;
     private final CarWatchdogDaemonHelper mCarWatchdogDaemonHelper;
@@ -99,6 +112,9 @@
             } else if (!action.equals(ACTION_GARAGE_MODE_OFF)) {
                 return;
             }
+            if (isGarageMode) {
+                mWatchdogStorage.shrinkDatabase();
+            }
             try {
                 mCarWatchdogDaemonHelper.notifySystemStateChange(StateType.GARAGE_MODE,
                         isGarageMode ? GarageMode.GARAGE_MODE_ON : GarageMode.GARAGE_MODE_OFF,
@@ -120,14 +136,20 @@
     private boolean mIsConnected;
 
     public CarWatchdogService(Context context) {
+        this(context, new WatchdogStorage(context));
+    }
+
+    @VisibleForTesting
+    CarWatchdogService(Context context, WatchdogStorage watchdogStorage) {
         mContext = context;
+        mWatchdogStorage = watchdogStorage;
         mPackageInfoHandler = new PackageInfoHandler(mContext.getPackageManager());
         mCarWatchdogDaemonHelper = new CarWatchdogDaemonHelper(TAG_WATCHDOG);
         mWatchdogServiceForSystem = new ICarWatchdogServiceForSystemImpl(this);
         mWatchdogProcessHandler = new WatchdogProcessHandler(mWatchdogServiceForSystem,
                 mCarWatchdogDaemonHelper);
         mWatchdogPerfHandler = new WatchdogPerfHandler(mContext, mCarWatchdogDaemonHelper,
-                mPackageInfoHandler);
+                mPackageInfoHandler, mWatchdogStorage);
         mConnectionListener = (isConnected) -> {
             mWatchdogPerfHandler.onDaemonConnectionChange(isConnected);
             synchronized (mLock) {
@@ -140,12 +162,12 @@
     @Override
     public void init() {
         mWatchdogProcessHandler.init();
+        mWatchdogPerfHandler.init();
         subscribePowerCycleChange();
         subscribeUserStateChange();
         subscribeBroadcastReceiver();
         mCarWatchdogDaemonHelper.addOnConnectionChangeListener(mConnectionListener);
         mCarWatchdogDaemonHelper.connect();
-        mWatchdogPerfHandler.init();
         // To make sure the main handler is ready for responding to car watchdog daemon, registering
         // to the daemon is done through the main handler. Once the registration is completed, we
         // can assume that the main handler is not too busy handling other stuffs.
@@ -158,7 +180,7 @@
     @Override
     public void release() {
         mContext.unregisterReceiver(mBroadcastReceiver);
-        mWatchdogPerfHandler.release();
+        mWatchdogStorage.release();
         unregisterFromDaemon();
         mCarWatchdogDaemonHelper.disconnect();
     }
@@ -199,11 +221,6 @@
         mWatchdogProcessHandler.tellClientAlive(client, sessionId);
     }
 
-    @VisibleForTesting
-    int getClientCount(int timeout) {
-        return mWatchdogProcessHandler.getClientCount(timeout);
-    }
-
     /** Returns {@link android.car.watchdog.ResourceOveruseStats} for the calling package. */
     @Override
     @NonNull
@@ -334,6 +351,16 @@
         mWatchdogProcessHandler.controlProcessHealthCheck(disable);
     }
 
+    @VisibleForTesting
+    int getClientCount(int timeout) {
+        return mWatchdogProcessHandler.getClientCount(timeout);
+    }
+
+    @VisibleForTesting
+    void setTimeSource(TimeSourceInterface timeSource) {
+        mWatchdogPerfHandler.setTimeSource(timeSource);
+    }
+
     private void postRegisterToDaemonMessage() {
         CarServiceUtils.runOnMain(() -> {
             synchronized (mLock) {
@@ -367,9 +394,9 @@
                 mCarWatchdogDaemonHelper.notifySystemStateChange(StateType.USER_STATE, info.id,
                         userState);
                 if (userState == UserState.USER_STATE_STOPPED) {
-                    mWatchdogProcessHandler.updateUserState(info.id, /*isStopped=*/true);
+                    mWatchdogProcessHandler.updateUserState(info.id, /*isStopped=*/ true);
                 } else {
-                    mWatchdogProcessHandler.updateUserState(info.id, /*isStopped=*/false);
+                    mWatchdogProcessHandler.updateUserState(info.id, /*isStopped=*/ false);
                 }
             }
         } catch (RemoteException | RuntimeException e) {
@@ -407,6 +434,7 @@
                     case CarPowerStateListener.SHUTDOWN_ENTER:
                     case CarPowerStateListener.SUSPEND_ENTER:
                         powerCycle = PowerCycle.POWER_CYCLE_SHUTDOWN_ENTER;
+                        mWatchdogPerfHandler.writeToDatabase();
                     // ON covers resume.
                     case CarPowerStateListener.ON:
                         powerCycle = PowerCycle.POWER_CYCLE_RESUME;
@@ -442,12 +470,12 @@
             String userStateDesc;
             switch (event.getEventType()) {
                 case USER_LIFECYCLE_EVENT_TYPE_STARTING:
-                    mWatchdogProcessHandler.updateUserState(userId, /*isStopped=*/false);
+                    mWatchdogProcessHandler.updateUserState(userId, /*isStopped=*/ false);
                     userState = UserState.USER_STATE_STARTED;
                     userStateDesc = "STARTING";
                     break;
                 case USER_LIFECYCLE_EVENT_TYPE_STOPPED:
-                    mWatchdogProcessHandler.updateUserState(userId, /*isStopped=*/true);
+                    mWatchdogProcessHandler.updateUserState(userId, /*isStopped=*/ true);
                     userState = UserState.USER_STATE_STOPPED;
                     userStateDesc = "STOPPING";
                     break;
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.aidl b/service/src/com/android/car/watchdog/TimeSourceInterface.java
similarity index 69%
copy from car-lib/src/android/car/telemetry/ManifestKey.aidl
copy to service/src/com/android/car/watchdog/TimeSourceInterface.java
index 25097df..1bd6508 100644
--- a/car-lib/src/android/car/telemetry/ManifestKey.aidl
+++ b/service/src/com/android/car/watchdog/TimeSourceInterface.java
@@ -14,9 +14,14 @@
  * limitations under the License.
  */
 
-package android.car.telemetry;
+package com.android.car.watchdog;
+
+import java.time.Instant;
 
 /**
- * @hide
+ * Provider for the current value of "now" for users of {@code java.time}.
  */
-parcelable ManifestKey;
\ No newline at end of file
+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 19523b6..fee73b9 100644
--- a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+++ b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
@@ -21,6 +21,11 @@
 import static android.automotive.watchdog.internal.ResourceOveruseActionType.NOT_KILLED;
 import static android.automotive.watchdog.internal.ResourceOveruseActionType.NOT_KILLED_USER_OPTED;
 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;
+import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_PAST_30_DAYS;
+import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_PAST_3_DAYS;
+import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS;
 import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_NEVER;
 import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_NO;
 import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_YES;
@@ -29,8 +34,11 @@
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
 
 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 android.annotation.NonNull;
 import android.annotation.UserIdInt;
@@ -63,6 +71,7 @@
 import android.content.pm.UserInfo;
 import android.os.Binder;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -76,9 +85,11 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
+import com.android.car.CarServiceUtils;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.TriConsumer;
 import com.android.server.utils.Slogf;
 
 import java.time.ZoneOffset;
@@ -108,10 +119,9 @@
     private final CarWatchdogDaemonHelper mCarWatchdogDaemonHelper;
     private final PackageInfoHandler mPackageInfoHandler;
     private final Handler mMainHandler;
+    private final HandlerThread mHandlerThread;
+    private final WatchdogStorage mWatchdogStorage;
     private final Object mLock = new Object();
-    /*
-     * Cache of added resource overuse listeners by uid.
-     */
     @GuardedBy("mLock")
     private final ArrayMap<String, PackageResourceUsage> mUsageByUserPackage = new ArrayMap<>();
     @GuardedBy("mLock")
@@ -132,22 +142,28 @@
     @GuardedBy("mLock")
     public final ArraySet<String> mDefaultNotKillableGenericPackages = new ArraySet<>();
     @GuardedBy("mLock")
-    private ZonedDateTime mLastStatsReportUTC;
+    private ZonedDateTime mLatestStatsReportDate;
     @GuardedBy("mLock")
     private List<android.automotive.watchdog.internal.ResourceOveruseConfiguration>
             mPendingSetResourceOveruseConfigurationsRequest = null;
     @GuardedBy("mLock")
     boolean mIsConnectedToDaemon;
     @GuardedBy("mLock")
+    boolean mIsWrittenToDatabase;
+    @GuardedBy("mLock")
+    private TimeSourceInterface mTimeSource;
+    @GuardedBy("mLock")
     long mResourceOveruseKillingDelayMills;
 
     public WatchdogPerfHandler(Context context, CarWatchdogDaemonHelper daemonHelper,
-            PackageInfoHandler packageInfoHandler) {
+            PackageInfoHandler packageInfoHandler, WatchdogStorage watchdogStorage) {
         mContext = context;
         mCarWatchdogDaemonHelper = daemonHelper;
         mPackageInfoHandler = packageInfoHandler;
         mMainHandler = new Handler(Looper.getMainLooper());
-        mLastStatsReportUTC = ZonedDateTime.now(ZoneOffset.UTC);
+        mHandlerThread = CarServiceUtils.getHandlerThread(CarWatchdogService.class.getSimpleName());
+        mWatchdogStorage = watchdogStorage;
+        mTimeSource = SYSTEM_INSTANCE;
         mResourceOveruseKillingDelayMills = RESOURCE_OVERUSE_KILLING_DELAY_MILLS;
     }
 
@@ -156,26 +172,19 @@
         /*
          * TODO(b/183947162): Opt-in to receive package change broadcast and handle package enabled
          *  state changes.
-         * TODO(b/192294393): Persist in-memory data: Read the current day's I/O overuse stats from
-         *  database.
          */
-        synchronized (mLock) {
-            checkAndHandleDateChangeLocked();
-        }
-        mMainHandler.post(this::fetchAndSyncResourceOveruseConfigurations);
+        /* First database read is expensive, so post it on a separate handler thread. */
+        mHandlerThread.getThreadHandler().post(() -> {
+            readFromDatabase();
+            synchronized (mLock) {
+                checkAndHandleDateChangeLocked();
+                mIsWrittenToDatabase = false;
+            }});
         if (DEBUG) {
             Slogf.d(TAG, "WatchdogPerfHandler is initialized");
         }
     }
 
-    /** Releases the handler */
-    public void release() {
-        /* TODO(b/192294393): Write daily usage to SQLite DB storage. */
-        if (DEBUG) {
-            Slogf.d(TAG, "WatchdogPerfHandler is released");
-        }
-    }
-
     /** Dumps its state. */
     public void dump(IndentingPrintWriter writer) {
         /*
@@ -185,15 +194,26 @@
 
     /** Retries any pending requests on re-connecting to the daemon */
     public void onDaemonConnectionChange(boolean isConnected) {
+        boolean hasPendingRequest;
         synchronized (mLock) {
             mIsConnectedToDaemon = isConnected;
+            hasPendingRequest = mPendingSetResourceOveruseConfigurationsRequest != null;
         }
         if (isConnected) {
-            /*
-             * Retry pending set resource overuse configuration request before processing any new
-             * set/get requests. Thus notify the waiting requests only after the retry completes.
-             */
-            retryPendingSetResourceOveruseConfigurations();
+            if (hasPendingRequest) {
+                /*
+                 * Retry pending set resource overuse configuration request before processing any
+                 * new set/get requests. Thus notify the waiting requests only after the retry
+                 * completes.
+                 */
+                retryPendingSetResourceOveruseConfigurations();
+            } else {
+                /* Start fetch/sync configs only when there are no pending set requests because the
+                 * above retry starts fetch/sync configs on success. If the retry fails, the daemon
+                 * has crashed and shouldn't start fetchAndSyncResourceOveruseConfigurations.
+                 */
+                mMainHandler.post(this::fetchAndSyncResourceOveruseConfigurations);
+            }
         }
         synchronized (mLock) {
             mLock.notifyAll();
@@ -373,7 +393,7 @@
             if (usage == null) {
                 usage = new PackageResourceUsage(userId, genericPackageName);
             }
-            if (!usage.setKillableStateLocked(isKillable)) {
+            if (!usage.verifyAndSetKillableStateLocked(isKillable)) {
                 Slogf.e(TAG, "User %d cannot set killable state for package '%s'",
                         userHandle.getIdentifier(), genericPackageName);
                 throw new IllegalArgumentException("Package killable state is not updatable");
@@ -402,7 +422,7 @@
                 if (usage == null) {
                     continue;
                 }
-                if (!usage.setKillableStateLocked(isKillable)) {
+                if (!usage.verifyAndSetKillableStateLocked(isKillable)) {
                     Slogf.e(TAG, "Cannot set killable state for package '%s'", packageName);
                     throw new IllegalArgumentException(
                             "Package killable state is not updatable");
@@ -448,7 +468,7 @@
 
     private List<PackageKillableState> getPackageKillableStatesForUserId(int userId,
             PackageManager pm) {
-        List<PackageInfo> packageInfos = pm.getInstalledPackagesAsUser(/* flags= */0, userId);
+        List<PackageInfo> packageInfos = pm.getInstalledPackagesAsUser(/* flags= */ 0, userId);
         List<PackageKillableState> states = new ArrayList<>();
         synchronized (mLock) {
             ArrayMap<String, List<ApplicationInfo>> applicationInfosBySharedPackage =
@@ -618,7 +638,6 @@
                     mOveruseActionsByUserPackage.add(overuseAction);
                     continue;
                 }
-
                 recurringIoOverusesByUid.put(stats.uid, hasRecurringOveruse);
             }
             if (!mOveruseActionsByUserPackage.isEmpty()) {
@@ -667,8 +686,6 @@
                 boolean hasRecurringOveruse = recurringIoOverusesByUid.valueAt(i);
                 String genericPackageName = genericPackageNamesByUid.get(uid);
                 int userId = UserHandle.getUserId(uid);
-                String key = getUserPackageUniqueId(userId, genericPackageName);
-                PackageResourceUsage usage = mUsageByUserPackage.get(key);
 
                 PackageResourceOveruseAction overuseAction = new PackageResourceOveruseAction();
                 overuseAction.packageIdentifier = new PackageIdentifier();
@@ -677,6 +694,13 @@
                 overuseAction.resourceTypes = new int[]{ ResourceType.IO };
                 overuseAction.resourceOveruseActionType = NOT_KILLED;
 
+                String key = getUserPackageUniqueId(userId, genericPackageName);
+                PackageResourceUsage usage = mUsageByUserPackage.get(key);
+                if (usage == null) {
+                    /* This case shouldn't happen but placed here as a fail safe. */
+                    mOveruseActionsByUserPackage.add(overuseAction);
+                    continue;
+                }
                 List<String> packages = Collections.singletonList(genericPackageName);
                 if (usage.isSharedPackage()) {
                     packages = mPackageInfoHandler.getPackagesForUid(uid, genericPackageName);
@@ -708,6 +732,10 @@
                                 userId, packageName);
                     }
                 }
+                if (overuseAction.resourceOveruseActionType == KILLED
+                        || overuseAction.resourceOveruseActionType == KILLED_RECURRING_OVERUSE) {
+                    usage.ioUsage.killed();
+                }
                 mOveruseActionsByUserPackage.add(overuseAction);
             }
             if (!mOveruseActionsByUserPackage.isEmpty()) {
@@ -732,7 +760,14 @@
         }
     }
 
-    /** Set the delay to kill a package after the package is notified of resource overuse. */
+    /** Sets the time source. */
+    public void setTimeSource(TimeSourceInterface timeSource) {
+        synchronized (mLock) {
+            mTimeSource = timeSource;
+        }
+    }
+
+    /** Sets the delay to kill a package after the package is notified of resource overuse. */
     public void setResourceOveruseKillingDelay(long millis) {
         synchronized (mLock) {
             mResourceOveruseKillingDelayMills = millis;
@@ -750,7 +785,7 @@
                 return;
             }
             if (internalConfigs.isEmpty()) {
-                Slogf.e(TAG, "Failed to fetch resource overuse configurations");
+                Slogf.e(TAG, "Fetched resource overuse configurations are empty");
                 return;
             }
             mSafeToKillSystemPackages.clear();
@@ -774,6 +809,103 @@
         }
     }
 
+    private void readFromDatabase() {
+        List<WatchdogStorage.UserPackageSettingsEntry> settingsEntries =
+                mWatchdogStorage.getUserPackageSettings();
+        Slogf.i(TAG, "Read %d user package settings from database", settingsEntries.size());
+        List<WatchdogStorage.IoUsageStatsEntry> ioStatsEntries =
+                mWatchdogStorage.getTodayIoUsageStats();
+        Slogf.i(TAG, "Read %d I/O usage stats from database", ioStatsEntries.size());
+        synchronized (mLock) {
+            for (int i = 0; i < settingsEntries.size(); ++i) {
+                WatchdogStorage.UserPackageSettingsEntry entry = settingsEntries.get(i);
+                if (entry.userId == UserHandle.USER_ALL) {
+                    if (entry.killableState != KILLABLE_STATE_YES) {
+                        mDefaultNotKillableGenericPackages.add(entry.packageName);
+                    }
+                    continue;
+                }
+                String key = getUserPackageUniqueId(entry.userId, entry.packageName);
+                PackageResourceUsage usage = mUsageByUserPackage.get(key);
+                if (usage == null) {
+                    usage = new PackageResourceUsage(entry.userId, entry.packageName);
+                }
+                usage.setKillableStateLocked(entry.killableState);
+                mUsageByUserPackage.put(key, usage);
+            }
+            ZonedDateTime curReportDate =
+                    mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+            for (int i = 0; i < ioStatsEntries.size(); ++i) {
+                WatchdogStorage.IoUsageStatsEntry entry = ioStatsEntries.get(i);
+                String key = getUserPackageUniqueId(entry.userId, entry.packageName);
+                PackageResourceUsage usage = mUsageByUserPackage.get(key);
+                if (usage == null) {
+                    usage = new PackageResourceUsage(entry.userId, entry.packageName);
+                }
+                /* Overwrite in memory cache as the stats will be merged on the daemon side and
+                 * pushed on the next latestIoOveruseStats call. This is tolerable because the next
+                 * push should happen soon.
+                 */
+                usage.ioUsage.overwrite(entry.ioUsage);
+                mUsageByUserPackage.put(key, usage);
+            }
+            if (!ioStatsEntries.isEmpty()) {
+                /* When mLatestStatsReportDate is null, the latest stats push from daemon hasn't
+                 * happened yet. Thus the cached stats contains only the stats read from database.
+                 */
+                mIsWrittenToDatabase = mLatestStatsReportDate == null;
+                mLatestStatsReportDate = curReportDate;
+            }
+        }
+    }
+
+    /** Writes user package settings and stats to database. */
+    public void writeToDatabase() {
+        synchronized (mLock) {
+            if (mIsWrittenToDatabase) {
+                return;
+            }
+            List<WatchdogStorage.UserPackageSettingsEntry>  entries =
+                    new ArrayList<>(mUsageByUserPackage.size());
+            for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+                PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+                entries.add(new WatchdogStorage.UserPackageSettingsEntry(
+                        usage.userId, usage.genericPackageName, usage.getKillableStateLocked()));
+            }
+            for (String packageName : mDefaultNotKillableGenericPackages) {
+                entries.add(new WatchdogStorage.UserPackageSettingsEntry(
+                        UserHandle.USER_ALL, packageName, KILLABLE_STATE_NO));
+            }
+            if (!mWatchdogStorage.saveUserPackageSettings(entries)) {
+                Slogf.e(TAG, "Failed to write user package settings to database");
+            } else {
+                Slogf.i(TAG, "Successfully saved %d user package settings to database",
+                        entries.size());
+            }
+            writeStatsLocked();
+            mIsWrittenToDatabase = true;
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void writeStatsLocked() {
+        List<WatchdogStorage.IoUsageStatsEntry> entries =
+                new ArrayList<>(mUsageByUserPackage.size());
+        for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+            PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+            if (!usage.ioUsage.hasUsage()) {
+                continue;
+            }
+            entries.add(new WatchdogStorage.IoUsageStatsEntry(
+                    usage.userId, usage.genericPackageName, usage.ioUsage));
+        }
+        if (!mWatchdogStorage.saveIoUsageStats(entries)) {
+            Slogf.e(TAG, "Failed to write %d I/O overuse stats to database", entries.size());
+        } else {
+            Slogf.i(TAG, "Successfully saved %d I/O overuse stats to database", entries.size());
+        }
+    }
+
     @GuardedBy("mLock")
     private int getPackageKillableStateForUserPackageLocked(
             int userId, String genericPackageName, int componentType, boolean isSafeToKill) {
@@ -813,12 +945,18 @@
 
     @GuardedBy("mLock")
     private void checkAndHandleDateChangeLocked() {
-        ZonedDateTime previousUTC = mLastStatsReportUTC;
-        mLastStatsReportUTC = ZonedDateTime.now(ZoneOffset.UTC);
-        if (mLastStatsReportUTC.getDayOfYear() == previousUTC.getDayOfYear()
-                && mLastStatsReportUTC.getYear() == previousUTC.getYear()) {
+        ZonedDateTime currentDate = mTimeSource.now().atZone(ZoneOffset.UTC)
+                .truncatedTo(STATS_TEMPORAL_UNIT);
+        if (currentDate.equals(mLatestStatsReportDate)) {
             return;
         }
+        /* After the first database read or on the first stats sync from the daemon, whichever
+         * happens first, the cached stats would either be empty or initialized from the database.
+         * In either case, don't write to database.
+         */
+        if (mLatestStatsReportDate != null && !mIsWrittenToDatabase) {
+            writeStatsLocked();
+        }
         for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
             PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
             // Forgive the daily disabled package on date change.
@@ -839,9 +977,9 @@
                                     usage.genericPackageName, usage.userId);
                 }
             }
-            /* TODO(b/192294393): Stash the old usage into SQLite DB storage. */
             usage.resetStatsLocked();
         }
+        mLatestStatsReportDate = currentDate;
         if (DEBUG) {
             Slogf.d(TAG, "Handled date change successfully");
         }
@@ -885,17 +1023,40 @@
     @GuardedBy("mLock")
     private IoOveruseStats getIoOveruseStatsLocked(PackageResourceUsage usage,
             long minimumBytesWritten, @CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
-        IoOveruseStats stats = usage.getIoOveruseStatsLocked();
-        long totalBytesWritten = stats != null ? stats.getTotalBytesWritten() : 0;
-        /*
-         * TODO(b/185431129): When maxStatsPeriod > current day, populate the historical stats
-         *  from the local database. Also handle the case where the package doesn't have current
-         *  day stats but has historical stats.
-         */
+        if (!usage.ioUsage.hasUsage()) {
+            /* Return I/O overuse stats only when the package has usage for the current day.
+             * Without the current day usage, the returned stats will contain zero remaining
+             * bytes, which is incorrect.
+             */
+            return null;
+        }
+        IoOveruseStats currentStats = usage.getIoOveruseStatsLocked();
+        long totalBytesWritten = currentStats.getTotalBytesWritten();
+        int numDays = toNumDays(maxStatsPeriod);
+        IoOveruseStats historyStats = null;
+        if (numDays > 0) {
+            historyStats = mWatchdogStorage.getHistoricalIoOveruseStats(
+                    usage.userId, usage.genericPackageName, numDays - 1);
+            totalBytesWritten += historyStats != null ? historyStats.getTotalBytesWritten() : 0;
+        }
         if (totalBytesWritten < minimumBytesWritten) {
             return null;
         }
-        return stats;
+        if (historyStats == null) {
+            return currentStats;
+        }
+        IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder(
+                historyStats.getStartTime(),
+                historyStats.getDurationInSeconds() + currentStats.getDurationInSeconds());
+        statsBuilder.setTotalTimesKilled(
+                historyStats.getTotalTimesKilled() + currentStats.getTotalTimesKilled());
+        statsBuilder.setTotalOveruses(
+                historyStats.getTotalOveruses() + currentStats.getTotalOveruses());
+        statsBuilder.setTotalBytesWritten(
+                historyStats.getTotalBytesWritten() + currentStats.getTotalBytesWritten());
+        statsBuilder.setKillableOnOveruse(currentStats.isKillableOnOveruse());
+        statsBuilder.setRemainingWriteBytes(currentStats.getRemainingWriteBytes());
+        return statsBuilder.build();
     }
 
     @GuardedBy("mLock")
@@ -1098,14 +1259,14 @@
 
     @VisibleForTesting
     static IoOveruseStats.Builder toIoOveruseStatsBuilder(
-            android.automotive.watchdog.IoOveruseStats internalStats) {
-        IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder(
-                internalStats.startTime, internalStats.durationInSeconds);
-        statsBuilder.setRemainingWriteBytes(
-                toPerStateBytes(internalStats.remainingWriteBytes));
-        statsBuilder.setTotalBytesWritten(totalPerStateBytes(internalStats.writtenBytes));
-        statsBuilder.setTotalOveruses(internalStats.totalOveruses);
-        return statsBuilder;
+            android.automotive.watchdog.IoOveruseStats internalStats,
+            int totalTimesKilled, boolean isKillableOnOveruses) {
+        return new IoOveruseStats.Builder(internalStats.startTime, internalStats.durationInSeconds)
+                .setTotalOveruses(internalStats.totalOveruses)
+                .setTotalTimesKilled(totalTimesKilled)
+                .setTotalBytesWritten(totalPerStateBytes(internalStats.writtenBytes))
+                .setKillableOnOveruse(isKillableOnOveruses)
+                .setRemainingWriteBytes(toPerStateBytes(internalStats.remainingWriteBytes));
     }
 
     private static PerStateBytes toPerStateBytes(
@@ -1304,15 +1465,23 @@
 
     private static IoOveruseConfiguration toIoOveruseConfiguration(
             android.automotive.watchdog.internal.IoOveruseConfiguration internalConfig) {
+        TriConsumer<Map<String, PerStateBytes>, String, String> replaceKey =
+                (map, oldKey, newKey) -> {
+                    PerStateBytes perStateBytes = map.get(oldKey);
+                    if (perStateBytes != null) {
+                        map.put(newKey, perStateBytes);
+                        map.remove(oldKey);
+                    }
+                };
         PerStateBytes componentLevelThresholds =
                 toPerStateBytes(internalConfig.componentLevelThresholds.perStateWriteBytes);
         ArrayMap<String, PerStateBytes> packageSpecificThresholds =
                 toPerStateBytesMap(internalConfig.packageSpecificThresholds);
         ArrayMap<String, PerStateBytes> appCategorySpecificThresholds =
                 toPerStateBytesMap(internalConfig.categorySpecificThresholds);
-        replaceKey(appCategorySpecificThresholds, INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS,
+        replaceKey.accept(appCategorySpecificThresholds, INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS,
                 ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MAPS);
-        replaceKey(appCategorySpecificThresholds, INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA,
+        replaceKey.accept(appCategorySpecificThresholds, INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA,
                 ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MEDIA);
         List<IoOveruseAlertThreshold> systemWideThresholds =
                 toIoOveruseAlertThresholds(internalConfig.systemWideThresholds);
@@ -1417,6 +1586,24 @@
         }
     }
 
+    private static int toNumDays(@CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
+        switch(maxStatsPeriod) {
+            case STATS_PERIOD_CURRENT_DAY:
+                return 0;
+            case STATS_PERIOD_PAST_3_DAYS:
+                return 3;
+            case STATS_PERIOD_PAST_7_DAYS:
+                return 7;
+            case STATS_PERIOD_PAST_15_DAYS:
+                return 15;
+            case STATS_PERIOD_PAST_30_DAYS:
+                return 30;
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid max stats period provided: " + maxStatsPeriod);
+        }
+    }
+
     private final class PackageResourceUsage {
         public final String genericPackageName;
         public @UserIdInt final int userId;
@@ -1470,8 +1657,7 @@
             if (!ioUsage.hasUsage()) {
                 return null;
             }
-            return ioUsage.getStatsBuilder().setKillableOnOveruse(
-                        mKillableState != KILLABLE_STATE_NEVER).build();
+            return ioUsage.getIoOveruseStats(mKillableState != KILLABLE_STATE_NEVER);
         }
 
         @GuardedBy("mLock")
@@ -1480,7 +1666,12 @@
         }
 
         @GuardedBy("mLock")
-        public boolean setKillableStateLocked(boolean isKillable) {
+        public void setKillableStateLocked(@KillableState int killableState) {
+            mKillableState = killableState;
+        }
+
+        @GuardedBy("mLock")
+        public boolean verifyAndSetKillableStateLocked(boolean isKillable) {
             if (mKillableState == KILLABLE_STATE_NEVER) {
                 return false;
             }
@@ -1492,7 +1683,7 @@
         public int syncAndFetchKillableStateLocked(int myComponentType, boolean isSafeToKill) {
             /*
              * The killable state goes out-of-sync:
-             * 1. When the on-device safe-to-kill list is recently updated and the user package
+             * 1. When the on-device safe-to-kill list was recently updated and the user package
              * didn't have any resource usage so the native daemon didn't update the killable state.
              * 2. When a package has no resource usage and is initialized outside of processing the
              * latest resource usage stats.
@@ -1512,21 +1703,48 @@
             ioUsage.resetStats();
         }
     }
-
-    private static final class PackageIoUsage {
+    /** Defines I/O usage fields for a package. */
+    public static final class PackageIoUsage {
         private android.automotive.watchdog.IoOveruseStats mIoOveruseStats;
         private android.automotive.watchdog.PerStateBytes mForgivenWriteBytes;
-        private long mTotalTimesKilled;
+        private int mTotalTimesKilled;
 
-        PackageIoUsage() {
+        private PackageIoUsage() {
+            mForgivenWriteBytes = new android.automotive.watchdog.PerStateBytes();
             mTotalTimesKilled = 0;
         }
 
-        public boolean hasUsage() {
+        public PackageIoUsage(android.automotive.watchdog.IoOveruseStats ioOveruseStats,
+                android.automotive.watchdog.PerStateBytes forgivenWriteBytes,
+                int totalTimesKilled) {
+            mIoOveruseStats = ioOveruseStats;
+            mForgivenWriteBytes = forgivenWriteBytes;
+            mTotalTimesKilled = totalTimesKilled;
+        }
+
+        public android.automotive.watchdog.IoOveruseStats getInternalIoOveruseStats() {
+            return mIoOveruseStats;
+        }
+
+        public android.automotive.watchdog.PerStateBytes getForgivenWriteBytes() {
+            return mForgivenWriteBytes;
+        }
+
+        public int getTotalTimesKilled() {
+            return mTotalTimesKilled;
+        }
+
+        boolean hasUsage() {
             return mIoOveruseStats != null;
         }
 
-        public void update(android.automotive.watchdog.IoOveruseStats internalStats) {
+        void overwrite(PackageIoUsage ioUsage) {
+            mIoOveruseStats = ioUsage.mIoOveruseStats;
+            mForgivenWriteBytes = ioUsage.mForgivenWriteBytes;
+            mTotalTimesKilled = ioUsage.mTotalTimesKilled;
+        }
+
+        void update(android.automotive.watchdog.IoOveruseStats internalStats) {
             mIoOveruseStats = internalStats;
             if (exceedsThreshold()) {
                 /*
@@ -1539,13 +1757,11 @@
             }
         }
 
-        public IoOveruseStats.Builder getStatsBuilder() {
-            IoOveruseStats.Builder statsBuilder = toIoOveruseStatsBuilder(mIoOveruseStats);
-            statsBuilder.setTotalTimesKilled(mTotalTimesKilled);
-            return statsBuilder;
+        IoOveruseStats getIoOveruseStats(boolean isKillable) {
+            return toIoOveruseStatsBuilder(mIoOveruseStats, mTotalTimesKilled, isKillable).build();
         }
 
-        public boolean exceedsThreshold() {
+        boolean exceedsThreshold() {
             if (!hasUsage()) {
                 return false;
             }
@@ -1555,7 +1771,11 @@
                     || remaining.garageModeBytes == 0;
         }
 
-        public void resetStats() {
+        void killed() {
+            ++mTotalTimesKilled;
+        }
+
+        void resetStats() {
             mIoOveruseStats = null;
             mForgivenWriteBytes = null;
             mTotalTimesKilled = 0;
diff --git a/service/src/com/android/car/watchdog/WatchdogStorage.java b/service/src/com/android/car/watchdog/WatchdogStorage.java
new file mode 100644
index 0000000..46445a5
--- /dev/null
+++ b/service/src/com/android/car/watchdog/WatchdogStorage.java
@@ -0,0 +1,657 @@
+/*
+ * 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.android.car.watchdog.CarWatchdogService.SYSTEM_INSTANCE;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.automotive.watchdog.PerStateBytes;
+import android.car.watchdog.IoOveruseStats;
+import android.car.watchdog.PackageKillableState.KillableState;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Process;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.car.CarLog;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+
+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.List;
+import java.util.Locale;
+
+/**
+ * Defines the database to store/retrieve system resource stats history from local storage.
+ */
+public final class WatchdogStorage {
+    private static final String TAG = CarLog.tagFor(WatchdogStorage.class);
+    private static final int RETENTION_PERIOD_IN_DAYS = 30;
+    /* Stats are stored on a daily basis. */
+    public static final TemporalUnit STATS_TEMPORAL_UNIT = ChronoUnit.DAYS;
+    /* 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;
+
+    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;
+
+    public WatchdogStorage(Context context) {
+        this(context, /* useDataSystemCarDir= */ true);
+    }
+
+    @VisibleForTesting
+    WatchdogStorage(Context context, boolean useDataSystemCarDir) {
+        mDbHelper = new WatchdogDbHelper(context, useDataSystemCarDir);
+    }
+
+    /** Releases resources. */
+    public void release() {
+        mDbHelper.close();
+    }
+
+    /** Handles database shrink. */
+    public void shrinkDatabase() {
+        try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+            mDbHelper.onShrink(db);
+        }
+    }
+
+    /** Saves the given user package settings entries and returns whether the change succeeded. */
+    public boolean saveUserPackageSettings(List<UserPackageSettingsEntry> entries) {
+        List<ContentValues> rows = new ArrayList<>(entries.size());
+        /* Capture only unique user ids. */
+        ArraySet<Integer> usersWithMissingIds = new ArraySet<>();
+        for (int i = 0; i < entries.size(); ++i) {
+            UserPackageSettingsEntry entry = entries.get(i);
+            if (mUserPackagesByKey.get(UserPackage.getKey(entry.userId, entry.packageName))
+                    == null) {
+                usersWithMissingIds.add(entry.userId);
+            }
+            rows.add(UserPackageSettingsTable.getContentValues(entry));
+        }
+        try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+            if (!atomicReplaceEntries(db, UserPackageSettingsTable.TABLE_NAME, rows)) {
+                return false;
+            }
+            populateUserPackages(db, usersWithMissingIds);
+        }
+        return true;
+    }
+
+    /** Returns the user package setting entries. */
+    public List<UserPackageSettingsEntry> getUserPackageSettings() {
+        ArrayMap<String, UserPackageSettingsEntry> entriesById;
+        try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+            entriesById = UserPackageSettingsTable.querySettings(db);
+        }
+        List<UserPackageSettingsEntry> entries = new ArrayList<>(entriesById.size());
+        for (int i = 0; i < entriesById.size(); ++i) {
+            String rowId = entriesById.keyAt(i);
+            UserPackageSettingsEntry entry = entriesById.valueAt(i);
+            UserPackage userPackage = new UserPackage(rowId, entry.userId, entry.packageName);
+            mUserPackagesByKey.put(userPackage.getKey(), userPackage);
+            mUserPackagesById.put(userPackage.getUniqueId(), userPackage);
+            entries.add(entry);
+        }
+        return entries;
+    }
+
+    /** Saves the given I/O usage stats. Returns true only on success. */
+    public boolean saveIoUsageStats(List<IoUsageStatsEntry> entries) {
+        return saveIoUsageStats(entries, /* shouldCheckRetention= */ true);
+    }
+
+    /** Returns the saved I/O usage stats for the current day. */
+    public List<IoUsageStatsEntry> getTodayIoUsageStats() {
+        ZonedDateTime statsDate =
+                mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+        long startEpochSeconds = statsDate.toEpochSecond();
+        long endEpochSeconds = mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond();
+        ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsagesById;
+        try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+            ioUsagesById = IoUsageStatsTable.queryStats(db, startEpochSeconds, endEpochSeconds);
+        }
+        List<IoUsageStatsEntry> entries = new ArrayList<>();
+        for (int i = 0; i < ioUsagesById.size(); ++i) {
+            String id = ioUsagesById.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;
+            }
+            entries.add(new IoUsageStatsEntry(userPackage.getUserId(), userPackage.getPackageName(),
+                    ioUsagesById.valueAt(i)));
+        }
+        return entries;
+    }
+
+    /**
+     * Returns the aggregated historical I/O overuse stats for the given user package or
+     * {@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();
+        try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+            UserPackage userPackage = mUserPackagesByKey.get(
+                    UserPackage.getKey(userId, packageName));
+            if (userPackage == null) {
+                /* Packages without historical stats don't have userPackage entry. */
+                return null;
+            }
+            return IoUsageStatsTable.queryHistoricalStats(
+                    db, userPackage.getUniqueId(), startEpochSeconds, endEpochSeconds);
+        }
+    }
+
+    @VisibleForTesting
+    boolean saveIoUsageStats(List<IoUsageStatsEntry> entries, boolean shouldCheckRetention) {
+        ZonedDateTime currentDate =
+                mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+        List<ContentValues> rows = new ArrayList<>(entries.size());
+        for (int i = 0; i < entries.size(); ++i) {
+            IoUsageStatsEntry entry = entries.get(i);
+            UserPackage userPackage = mUserPackagesByKey.get(
+                    UserPackage.getKey(entry.userId, entry.packageName));
+            if (userPackage == null) {
+                Slogf.i(TAG, "Failed to find unique database id for user id '%d' and package '%s",
+                        entry.userId, entry.packageName);
+                continue;
+            }
+            android.automotive.watchdog.IoOveruseStats ioOveruseStats =
+                    entry.ioUsage.getInternalIoOveruseStats();
+            ZonedDateTime statsDate = Instant.ofEpochSecond(ioOveruseStats.startTime)
+                    .atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+            if (shouldCheckRetention && STATS_TEMPORAL_UNIT.between(statsDate, currentDate)
+                    >= RETENTION_PERIOD.get(STATS_TEMPORAL_UNIT)) {
+                continue;
+            }
+            long statsDateEpochSeconds = statsDate.toEpochSecond();
+            rows.add(IoUsageStatsTable.getContentValues(
+                    userPackage.getUniqueId(), entry, statsDateEpochSeconds));
+        }
+        try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+            return atomicReplaceEntries(db, IoUsageStatsTable.TABLE_NAME, rows);
+        }
+    }
+
+    @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) {
+            UserPackage userPackage = userPackages.get(i);
+            mUserPackagesByKey.put(userPackage.getKey(), userPackage);
+            mUserPackagesById.put(userPackage.getUniqueId(), userPackage);
+        }
+    }
+
+    private static boolean atomicReplaceEntries(
+            SQLiteDatabase db, String tableName, List<ContentValues> rows) {
+        if (rows.isEmpty()) {
+            return true;
+        }
+        try {
+            db.beginTransaction();
+            for (int i = 0; i < rows.size(); ++i) {
+                try {
+                    if (db.replaceOrThrow(tableName, null, rows.get(i)) == -1) {
+                        Slogf.e(TAG, "Failed to insert %s entry [%s]", tableName, rows.get(i));
+                        return false;
+                    }
+                } catch (SQLException e) {
+                    Slog.e(TAG, "Failed to insert " + tableName + " entry [" + rows.get(i) + "]",
+                            e);
+                    return false;
+                }
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+        return true;
+    }
+
+    /** 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 @KillableState int killableState;
+
+        UserPackageSettingsEntry(
+                @UserIdInt int userId, String packageName, @KillableState int killableState) {
+            this.userId = userId;
+            this.packageName = packageName;
+            this.killableState = killableState;
+        }
+    }
+
+    /**
+     * Defines the contents and queries for the user package settings table.
+     */
+    static final class UserPackageSettingsTable {
+        public static final String TABLE_NAME = "user_package_settings";
+        public static final String COLUMN_ROWID = "rowid";
+        public static final String COLUMN_PACKAGE_NAME = "package_name";
+        public static final String COLUMN_USER_ID = "user_id";
+        public static final String COLUMN_KILLABLE_STATE = "killable_state";
+
+        public static void createTable(SQLiteDatabase db) {
+            StringBuilder createCommand = new StringBuilder();
+            createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (")
+                    .append(COLUMN_PACKAGE_NAME).append(" TEXT NOT NULL, ")
+                    .append(COLUMN_USER_ID).append(" INTEGER NOT NULL, ")
+                    .append(COLUMN_KILLABLE_STATE).append(" INTEGER NOT NULL, ")
+                    .append("PRIMARY KEY (").append(COLUMN_PACKAGE_NAME)
+                    .append(", ").append(COLUMN_USER_ID).append("))");
+            db.execSQL(createCommand.toString());
+            Slogf.i(TAG, "Successfully created the %s table in the %s database version %d",
+                    TABLE_NAME, WatchdogDbHelper.DATABASE_NAME, WatchdogDbHelper.DATABASE_VERSION);
+        }
+
+        public static ContentValues getContentValues(UserPackageSettingsEntry entry) {
+            ContentValues values = new ContentValues();
+            values.put(COLUMN_USER_ID, entry.userId);
+            values.put(COLUMN_PACKAGE_NAME, entry.packageName);
+            values.put(COLUMN_KILLABLE_STATE, entry.killableState);
+            return values;
+        }
+
+        public static ArrayMap<String, UserPackageSettingsEntry> querySettings(SQLiteDatabase db) {
+            StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.append("SELECT ")
+                    .append(COLUMN_ROWID).append(", ")
+                    .append(COLUMN_USER_ID).append(", ")
+                    .append(COLUMN_PACKAGE_NAME).append(", ")
+                    .append(COLUMN_KILLABLE_STATE)
+                    .append(" FROM ").append(TABLE_NAME);
+
+            try (Cursor cursor = db.rawQuery(queryBuilder.toString(), new String[]{})) {
+                ArrayMap<String, UserPackageSettingsEntry> entriesById = new ArrayMap<>(
+                        cursor.getCount());
+                while (cursor.moveToNext()) {
+                    entriesById.put(cursor.getString(0), new UserPackageSettingsEntry(
+                            cursor.getInt(1), cursor.getString(2), cursor.getInt(3)));
+                }
+                return entriesById;
+            }
+        }
+
+        public static List<UserPackage> queryUserPackages(
+                SQLiteDatabase db, ArraySet<Integer> users) {
+            StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.append("SELECT ")
+                    .append(COLUMN_ROWID).append(", ")
+                    .append(COLUMN_USER_ID).append(", ")
+                    .append(COLUMN_PACKAGE_NAME)
+                    .append(" FROM ").append(TABLE_NAME);
+            for (int i = 0; i < users.size(); ++i) {
+                if (i == 0) {
+                    queryBuilder.append(" WHERE ").append(COLUMN_USER_ID).append(" IN (");
+                } else {
+                    queryBuilder.append(", ");
+                }
+                queryBuilder.append(users.valueAt(i));
+                if (i == users.size() - 1) {
+                    queryBuilder.append(")");
+                }
+            }
+
+            try (Cursor cursor = db.rawQuery(queryBuilder.toString(), new String[]{})) {
+                List<UserPackage> userPackages = new ArrayList<>(cursor.getCount());
+                while (cursor.moveToNext()) {
+                    userPackages.add(new UserPackage(
+                            cursor.getString(0), cursor.getInt(1), cursor.getString(2)));
+                }
+                return userPackages;
+            }
+        }
+    }
+
+    /** Defines the I/O usage entry stored in the IoUsageStatsTable. */
+    static final class IoUsageStatsEntry {
+        public final @UserIdInt int userId;
+        public final String packageName;
+        public final WatchdogPerfHandler.PackageIoUsage ioUsage;
+
+        IoUsageStatsEntry(@UserIdInt int userId,
+                String packageName, WatchdogPerfHandler.PackageIoUsage ioUsage) {
+            this.userId = userId;
+            this.packageName = packageName;
+            this.ioUsage = ioUsage;
+        }
+    }
+
+    /**
+     * Defines the contents and queries for the I/O usage stats table.
+     */
+    static final class IoUsageStatsTable {
+        public static final String TABLE_NAME = "io_usage_stats";
+        public static final String COLUMN_USER_PACKAGE_ID = "user_package_id";
+        public static final String COLUMN_DATE_EPOCH = "date_epoch";
+        public static final String COLUMN_NUM_OVERUSES = "num_overuses";
+        public static final String COLUMN_NUM_FORGIVEN_OVERUSES =  "num_forgiven_overuses";
+        public static final String COLUMN_NUM_TIMES_KILLED = "num_times_killed";
+        public static final String COLUMN_WRITTEN_FOREGROUND_BYTES = "written_foreground_bytes";
+        public static final String COLUMN_WRITTEN_BACKGROUND_BYTES = "written_background_bytes";
+        public static final String COLUMN_WRITTEN_GARAGE_MODE_BYTES = "written_garage_mode_bytes";
+        /* Below columns will be null for historical stats i.e., when the date != current date. */
+        public static final String COLUMN_REMAINING_FOREGROUND_WRITE_BYTES =
+                "remaining_foreground_write_bytes";
+        public static final String COLUMN_REMAINING_BACKGROUND_WRITE_BYTES =
+                "remaining_background_write_bytes";
+        public static final String COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES =
+                "remaining_garage_mode_write_bytes";
+        public static final String COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES =
+                "forgiven_foreground_write_bytes";
+        public static final String COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES =
+                "forgiven_background_write_bytes";
+        public static final String COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES =
+                "forgiven_garage_mode_write_bytes";
+
+        public static void createTable(SQLiteDatabase db) {
+            StringBuilder createCommand = new StringBuilder();
+            createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (")
+                    .append(COLUMN_USER_PACKAGE_ID).append(" INTEGER NOT NULL, ")
+                    .append(COLUMN_DATE_EPOCH).append(" INTEGER NOT NULL, ")
+                    .append(COLUMN_NUM_OVERUSES).append(" INTEGER NOT NULL, ")
+                    .append(COLUMN_NUM_FORGIVEN_OVERUSES).append(" INTEGER NOT NULL, ")
+                    .append(COLUMN_NUM_TIMES_KILLED).append(" INTEGER NOT NULL, ")
+                    .append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" INTEGER, ")
+                    .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" INTEGER, ")
+                    .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(" INTEGER, ")
+                    .append(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES).append(" INTEGER, ")
+                    .append(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES).append(" INTEGER, ")
+                    .append(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES).append(" INTEGER, ")
+                    .append(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES).append(" INTEGER, ")
+                    .append(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES).append(" INTEGER, ")
+                    .append(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES).append(" INTEGER, ")
+                    .append("PRIMARY KEY (").append(COLUMN_USER_PACKAGE_ID).append(", ")
+                    .append(COLUMN_DATE_EPOCH).append("), FOREIGN KEY (")
+                    .append(COLUMN_USER_PACKAGE_ID).append(") REFERENCES ")
+                    .append(UserPackageSettingsTable.TABLE_NAME).append(" (")
+                    .append(UserPackageSettingsTable.COLUMN_ROWID).append(") ON DELETE CASCADE )");
+            db.execSQL(createCommand.toString());
+            Slogf.i(TAG, "Successfully created the %s table in the %s database version %d",
+                    TABLE_NAME, WatchdogDbHelper.DATABASE_NAME, WatchdogDbHelper.DATABASE_VERSION);
+        }
+
+        public static ContentValues getContentValues(
+                String userPackageId, IoUsageStatsEntry entry, long statsDateEpochSeconds) {
+            android.automotive.watchdog.IoOveruseStats ioOveruseStats =
+                    entry.ioUsage.getInternalIoOveruseStats();
+            ContentValues values = new ContentValues();
+            values.put(COLUMN_USER_PACKAGE_ID, userPackageId);
+            values.put(COLUMN_DATE_EPOCH, statsDateEpochSeconds);
+            values.put(COLUMN_NUM_OVERUSES, ioOveruseStats.totalOveruses);
+            /* TODO(b/195425666): Put total forgiven overuses for the day. */
+            values.put(COLUMN_NUM_FORGIVEN_OVERUSES, 0);
+            values.put(COLUMN_NUM_TIMES_KILLED, entry.ioUsage.getTotalTimesKilled());
+            values.put(
+                    COLUMN_WRITTEN_FOREGROUND_BYTES, ioOveruseStats.writtenBytes.foregroundBytes);
+            values.put(
+                    COLUMN_WRITTEN_BACKGROUND_BYTES, ioOveruseStats.writtenBytes.backgroundBytes);
+            values.put(
+                    COLUMN_WRITTEN_GARAGE_MODE_BYTES, ioOveruseStats.writtenBytes.garageModeBytes);
+            values.put(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES,
+                    ioOveruseStats.remainingWriteBytes.foregroundBytes);
+            values.put(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES,
+                    ioOveruseStats.remainingWriteBytes.backgroundBytes);
+            values.put(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES,
+                    ioOveruseStats.remainingWriteBytes.garageModeBytes);
+            android.automotive.watchdog.PerStateBytes forgivenWriteBytes =
+                    entry.ioUsage.getForgivenWriteBytes();
+            values.put(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES, forgivenWriteBytes.foregroundBytes);
+            values.put(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES, forgivenWriteBytes.backgroundBytes);
+            values.put(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES, forgivenWriteBytes.garageModeBytes);
+            return values;
+        }
+
+        public static ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> queryStats(
+                SQLiteDatabase db, long startEpochSeconds, long endEpochSeconds) {
+            StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.append("SELECT ")
+                    .append(COLUMN_USER_PACKAGE_ID).append(", ")
+                    .append("MIN(").append(COLUMN_DATE_EPOCH).append("), ")
+                    .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
+                    .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).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("SUM(").append(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES).append(") ")
+                    .append("FROM ").append(TABLE_NAME).append(" WHERE ")
+                    .append(COLUMN_DATE_EPOCH).append(">= ? and ")
+                    .append(COLUMN_DATE_EPOCH).append("< ? GROUP BY ")
+                    .append(COLUMN_USER_PACKAGE_ID);
+            String[] selectionArgs = new String[]{
+                    String.valueOf(startEpochSeconds), String.valueOf(endEpochSeconds)};
+
+            ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsageById = new ArrayMap<>();
+            try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
+                while (cursor.moveToNext()) {
+                    android.automotive.watchdog.IoOveruseStats ioOveruseStats =
+                            new android.automotive.watchdog.IoOveruseStats();
+                    ioOveruseStats.startTime = cursor.getLong(1);
+                    ioOveruseStats.durationInSeconds = endEpochSeconds - startEpochSeconds;
+                    ioOveruseStats.totalOveruses = cursor.getInt(2);
+                    ioOveruseStats.writtenBytes = new PerStateBytes();
+                    ioOveruseStats.writtenBytes.foregroundBytes = cursor.getLong(4);
+                    ioOveruseStats.writtenBytes.backgroundBytes = cursor.getLong(5);
+                    ioOveruseStats.writtenBytes.garageModeBytes = cursor.getLong(6);
+                    ioOveruseStats.remainingWriteBytes = new PerStateBytes();
+                    ioOveruseStats.remainingWriteBytes.foregroundBytes = cursor.getLong(7);
+                    ioOveruseStats.remainingWriteBytes.backgroundBytes = cursor.getLong(8);
+                    ioOveruseStats.remainingWriteBytes.garageModeBytes = cursor.getLong(9);
+                    PerStateBytes forgivenWriteBytes = new PerStateBytes();
+                    forgivenWriteBytes.foregroundBytes = cursor.getLong(10);
+                    forgivenWriteBytes.backgroundBytes = cursor.getLong(11);
+                    forgivenWriteBytes.garageModeBytes = cursor.getLong(12);
+
+                    ioUsageById.put(cursor.getString(0), new WatchdogPerfHandler.PackageIoUsage(
+                            ioOveruseStats, forgivenWriteBytes, cursor.getInt(3)));
+                }
+            }
+            return ioUsageById;
+        }
+
+        public static IoOveruseStats queryHistoricalStats(SQLiteDatabase db, String uniqueId,
+                long startEpochSeconds, long endEpochSeconds) {
+            StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.append("SELECT SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
+                    .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
+                    .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
+                    .append("MIN(").append(COLUMN_DATE_EPOCH).append(") ")
+                    .append("FROM ").append(TABLE_NAME).append(" WHERE ")
+                    .append(COLUMN_USER_PACKAGE_ID).append("=? and ")
+                    .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
+                    .append(COLUMN_DATE_EPOCH).append("< ?");
+            String[] selectionArgs = new String[]{uniqueId,
+                    String.valueOf(startEpochSeconds), String.valueOf(endEpochSeconds)};
+            long totalOveruses = 0;
+            long totalTimesKilled = 0;
+            long totalBytesWritten = 0;
+            long earliestEpochSecond = endEpochSeconds;
+            try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
+                if (cursor.getCount() == 0) {
+                    return null;
+                }
+                while (cursor.moveToNext()) {
+                    totalOveruses += cursor.getLong(0);
+                    totalTimesKilled += cursor.getLong(1);
+                    totalBytesWritten += cursor.getLong(2) + cursor.getLong(3) + cursor.getLong(4);
+                    earliestEpochSecond = Math.min(cursor.getLong(5), earliestEpochSecond);
+                }
+            }
+            if (totalBytesWritten == 0) {
+                return null;
+            }
+            long durationInSeconds = endEpochSeconds - earliestEpochSecond;
+            IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder(
+                    earliestEpochSecond, durationInSeconds);
+            statsBuilder.setTotalOveruses(totalOveruses);
+            statsBuilder.setTotalTimesKilled(totalTimesKilled);
+            statsBuilder.setTotalBytesWritten(totalBytesWritten);
+            return statsBuilder.build();
+        }
+
+        public static void truncateToDate(SQLiteDatabase db, ZonedDateTime latestTruncateDate) {
+            String selection = COLUMN_DATE_EPOCH + " <= ?";
+            String[] selectionArgs = { String.valueOf(latestTruncateDate.toEpochSecond()) };
+
+            int rows = db.delete(TABLE_NAME, selection, selectionArgs);
+            Slogf.i(TAG, "Truncated %d I/O usage stats entries on pid %d", rows, Process.myPid());
+        }
+
+        public static void trimHistoricalStats(SQLiteDatabase db, ZonedDateTime currentDate) {
+            ContentValues values = new ContentValues();
+            values.putNull(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES);
+            values.putNull(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES);
+            values.putNull(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES);
+            values.putNull(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES);
+            values.putNull(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES);
+            values.putNull(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES);
+
+            String selection = COLUMN_DATE_EPOCH + " < ?";
+            String[] selectionArgs = { String.valueOf(currentDate.toEpochSecond()) };
+
+            int rows = db.update(TABLE_NAME, values, selection, selectionArgs);
+            Slogf.i(TAG, "Trimmed %d I/O usage stats entries on pid %d", rows, Process.myPid());
+        }
+    }
+
+    /**
+     * 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;
+
+        WatchdogDbHelper(Context context, boolean useDataSystemCarDir) {
+            /* 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,
+                    /* name= */ null, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            UserPackageSettingsTable.createTable(db);
+            IoUsageStatsTable.createTable(db);
+        }
+
+        public synchronized void close() {
+            super.close();
+
+            mLatestShrinkDate = null;
+        }
+
+        public void onShrink(SQLiteDatabase db) {
+            ZonedDateTime currentDate =
+                    mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(ChronoUnit.DAYS);
+            if (currentDate.equals(mLatestShrinkDate)) {
+                return;
+            }
+            IoUsageStatsTable.truncateToDate(db, currentDate.minus(RETENTION_PERIOD));
+            IoUsageStatsTable.trimHistoricalStats(db, currentDate);
+            mLatestShrinkDate = currentDate;
+            Slogf.i(TAG, "Shrunk watchdog database for the date '%s'", mLatestShrinkDate);
+        }
+
+        @Override
+        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 {
+        private final String mUniqueId;
+        private final int mUserId;
+        private final String mPackageName;
+
+        UserPackage(String uniqueId, int userId, String packageName) {
+            mUniqueId = uniqueId;
+            mUserId = userId;
+            mPackageName = packageName;
+        }
+
+        String getKey() {
+            return getKey(mUserId, mPackageName);
+        }
+
+        static String getKey(int userId, String packageName) {
+            return String.format(Locale.ENGLISH, "%d:%s", userId, packageName);
+        }
+
+        public String getUniqueId() {
+            return mUniqueId;
+        }
+
+        public int getUserId() {
+            return mUserId;
+        }
+
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+    }
+}
diff --git a/tests/CarEvsCameraPreviewApp/res/values/config.xml b/tests/CarEvsCameraPreviewApp/res/values/config.xml
new file mode 100644
index 0000000..935c096
--- /dev/null
+++ b/tests/CarEvsCameraPreviewApp/res/values/config.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.
+  -->
+<resources>
+    <!-- Shade of the background behind the camera window. 1.0 for fully opaque, 0.0 for fully
+         transparent. -->
+    <item name="config_cameraBackgroundScrim" format="float" type="dimen">0.7</item>
+</resources>
\ No newline at end of file
diff --git a/tests/CarEvsCameraPreviewApp/res/values/overlayable.xml b/tests/CarEvsCameraPreviewApp/res/values/overlayable.xml
new file mode 100644
index 0000000..0f77eab
--- /dev/null
+++ b/tests/CarEvsCameraPreviewApp/res/values/overlayable.xml
@@ -0,0 +1,33 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- 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.-->
+<!--
+THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY.
+REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py
+-->
+<resources>
+  <overlayable name="CarEvsCameraPreviewApp">
+    <policy type="system|product|signature">
+      <item type="color" name="button_background"/>
+      <item type="color" name="button_text"/>
+      <item type="dimen" name="camera_preview_height"/>
+      <item type="dimen" name="camera_preview_width"/>
+      <item type="dimen" name="close_button_text_size"/>
+      <item type="dimen" name="config_cameraBackgroundScrim"/>
+      <item type="id" name="close_button"/>
+      <item type="id" name="evs_preview_container"/>
+      <item type="layout" name="evs_preview_activity"/>
+      <item type="string" name="app_name"/>
+      <item type="string" name="close_button_text"/>
+      <item type="style" name="Theme.Transparent"/>
+    </policy>
+  </overlayable>
+</resources>
diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
index 737d918..6ff7940 100644
--- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
+++ b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
@@ -34,8 +34,8 @@
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.WindowManager;
-import android.widget.Button;
 import android.widget.LinearLayout;
 
 import java.util.ArrayList;
@@ -55,6 +55,7 @@
 
     /** GL backed surface view to render the camera preview */
     private CarEvsCameraGLSurfaceView mEvsView;
+    private ViewGroup mRootView;
     private LinearLayout mPreviewContainer;
 
     /** Display manager to monitor the display's state */
@@ -164,8 +165,9 @@
                 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
 
         mEvsView = new CarEvsCameraGLSurfaceView(getApplication(), this);
-        mPreviewContainer = (LinearLayout) LayoutInflater.from(this).inflate(
+        mRootView = (ViewGroup) LayoutInflater.from(this).inflate(
                 R.layout.evs_preview_activity, /* root= */ null);
+        mPreviewContainer = mRootView.findViewById(R.id.evs_preview_container);
         LinearLayout.LayoutParams viewParam = new LinearLayout.LayoutParams(
                 LinearLayout.LayoutParams.MATCH_PARENT,
                 LinearLayout.LayoutParams.MATCH_PARENT,
@@ -173,31 +175,31 @@
         );
         mEvsView.setLayoutParams(viewParam);
         mPreviewContainer.addView(mEvsView, 0);
-        Button closeButton = mPreviewContainer.findViewById(R.id.close_button);
-        closeButton.setOnClickListener((v) -> finish());
+        View closeButton = mRootView.findViewById(R.id.close_button);
+        if (closeButton != null) {
+            closeButton.setOnClickListener(v -> finish());
+        }
 
         int width = WindowManager.LayoutParams.MATCH_PARENT;
         int height = WindowManager.LayoutParams.MATCH_PARENT;
-        int x = 0;
-        int y = 0;
         if (mUseSystemWindow) {
             width = getResources().getDimensionPixelOffset(R.dimen.camera_preview_width);
             height = getResources().getDimensionPixelOffset(R.dimen.camera_preview_height);
-            x = (getResources().getDisplayMetrics().widthPixels - width) / 2;
-            y = (getResources().getDisplayMetrics().heightPixels - height) / 2;
         }
         WindowManager.LayoutParams params = new WindowManager.LayoutParams(
-                width, height, x, y,
+                width, height,
                 2020 /* WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY */,
-                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+                WindowManager.LayoutParams.FLAG_DIM_BEHIND
                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                 PixelFormat.TRANSLUCENT);
-        params.gravity = Gravity.LEFT | Gravity.TOP;
+        params.gravity = Gravity.CENTER;
+        params.dimAmount = getResources().getFloat(R.dimen.config_cameraBackgroundScrim);
+
         if (mUseSystemWindow) {
             WindowManager wm = getSystemService(WindowManager.class);
-            wm.addView(mPreviewContainer, params);
+            wm.addView(mRootView, params);
         } else {
-            setContentView(mPreviewContainer, params);
+            setContentView(mRootView, params);
         }
     }
 
@@ -252,7 +254,7 @@
         mDisplayManager.unregisterDisplayListener(mDisplayListener);
         if (mUseSystemWindow) {
             WindowManager wm = getSystemService(WindowManager.class);
-            wm.removeView(mPreviewContainer);
+            wm.removeView(mRootView);
         }
     }
 
diff --git a/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java b/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java
deleted file mode 100644
index 906662d..0000000
--- a/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java
+++ /dev/null
@@ -1,175 +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 android.car;
-
-import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
-import static android.car.telemetry.CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.verify;
-
-import android.app.Application;
-import android.car.telemetry.CarTelemetryManager;
-import android.car.telemetry.ManifestKey;
-import android.car.testapi.CarTelemetryController;
-import android.car.testapi.FakeCar;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.internal.DoNotInstrument;
-
-import java.util.concurrent.Executor;
-
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-public class CarTelemetryManagerTest {
-    @Rule
-    public MockitoRule rule = MockitoJUnit.rule();
-
-    private static final byte[] ERROR_BYTES = "ERROR".getBytes();
-    private static final byte[] MANIFEST_BYTES = "MANIFEST".getBytes();
-    private static final byte[] SCRIPT_RESULT_BYTES = "SCRIPT RESULT".getBytes();
-    private static final ManifestKey DEFAULT_MANIFEST_KEY =
-            new ManifestKey("NAME", 1);
-    private static final Executor DIRECT_EXECUTOR = Runnable::run;
-
-    private CarTelemetryController mCarTelemetryController;
-    private CarTelemetryManager mCarTelemetryManager;
-
-    @Mock
-    private CarTelemetryManager.CarTelemetryResultsListener mListener;
-
-
-
-    @Before
-    public void setUp() {
-        Application context = ApplicationProvider.getApplicationContext();
-        FakeCar fakeCar = FakeCar.createFakeCar(context);
-        Car carApi = fakeCar.getCar();
-
-        mCarTelemetryManager =
-                (CarTelemetryManager) carApi.getCarManager(Car.CAR_TELEMETRY_SERVICE);
-        mCarTelemetryController = fakeCar.getCarTelemetryController();
-    }
-
-    @Test
-    public void setListener_shouldSucceed() {
-        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
-
-        assertThat(mCarTelemetryController.isListenerSet()).isTrue();
-    }
-
-    @Test
-    public void clearListener_shouldSucceed() {
-        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
-        mCarTelemetryManager.clearListener();
-
-        assertThat(mCarTelemetryController.isListenerSet()).isFalse();
-    }
-
-    @Test
-    public void addManifest_whenNew_shouldSucceed() {
-        int result = mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-
-        assertThat(result).isEqualTo(ERROR_NONE);
-        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
-    }
-
-    @Test
-    public void addManifest_whenDuplicate_shouldIgnore() {
-        int firstResult =
-                mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-        int secondResult =
-                mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-
-        assertThat(firstResult).isEqualTo(ERROR_NONE);
-        assertThat(secondResult).isEqualTo(ERROR_SAME_MANIFEST_EXISTS);
-        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
-    }
-
-    @Test
-    public void removeManifest_whenValid_shouldSucceed() {
-        mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-
-        boolean result = mCarTelemetryManager.removeManifest(DEFAULT_MANIFEST_KEY);
-
-        assertThat(result).isTrue();
-        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(0);
-    }
-
-    @Test
-    public void removeManifest_whenInvalid_shouldIgnore() {
-        mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-
-        boolean result = mCarTelemetryManager.removeManifest(new ManifestKey("NAME", 100));
-
-        assertThat(result).isFalse();
-        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
-    }
-
-    @Test
-    public void removeAllManifests_shouldSucceed() {
-        mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-        mCarTelemetryManager.addManifest(new ManifestKey("NAME", 100), MANIFEST_BYTES);
-
-        mCarTelemetryManager.removeAllManifests();
-
-        assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(0);
-    }
-
-    @Test
-    public void sendFinishedReports_shouldSucceed() {
-        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
-        mCarTelemetryController.addDataForKey(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
-
-        mCarTelemetryManager.sendFinishedReports(DEFAULT_MANIFEST_KEY);
-
-        verify(mListener).onResult(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
-    }
-
-    @Test
-    public void sendAllFinishedReports_shouldSucceed() {
-        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
-        mCarTelemetryController.addDataForKey(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
-        ManifestKey key2 = new ManifestKey("key name", 1);
-        mCarTelemetryController.addDataForKey(key2, SCRIPT_RESULT_BYTES);
-
-        mCarTelemetryManager.sendAllFinishedReports();
-
-        verify(mListener).onResult(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
-        verify(mListener).onResult(key2, SCRIPT_RESULT_BYTES);
-    }
-
-    @Test
-    public void sendScriptExecutionErrors_shouldSucceed() {
-        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
-        mCarTelemetryController.setErrorData(ERROR_BYTES);
-
-        mCarTelemetryManager.sendScriptExecutionErrors();
-
-        verify(mListener).onError(ERROR_BYTES);
-    }
-}
diff --git a/tests/CarSecurityPermissionTest/src/com/android/car/input/CarInputManagerPermisisonTest.java b/tests/CarSecurityPermissionTest/src/com/android/car/input/CarInputManagerPermisisonTest.java
index 01cff52..466333b 100644
--- a/tests/CarSecurityPermissionTest/src/com/android/car/input/CarInputManagerPermisisonTest.java
+++ b/tests/CarSecurityPermissionTest/src/com/android/car/input/CarInputManagerPermisisonTest.java
@@ -17,6 +17,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.mock;
 import static org.testng.Assert.assertThrows;
 import static org.testng.Assert.expectThrows;
 
@@ -35,6 +36,8 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.util.concurrent.Executor;
+
 /**
  * This class contains security permission tests for the {@link CarInputManager}'s system APIs.
  */
@@ -62,14 +65,22 @@
     }
 
     @Test
-    public void testEnableFeaturePermission() {
+    public void testRequestInputEventCapturePermission() {
         assertThrows(SecurityException.class, () -> mCarInputManager.requestInputEventCapture(
                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
                 new int[]{CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION}, 0, mMockedCallback));
     }
 
     @Test
-    public void testInjectKeyEvent() {
+    public void testRequestInputEventCaptureWithExecutorPermission() {
+        assertThrows(SecurityException.class, () -> mCarInputManager.requestInputEventCapture(
+                CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
+                new int[]{CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION}, 0,
+                mock(Executor.class), mMockedCallback));
+    }
+
+    @Test
+    public void testInjectKeyEventPermission() {
         long currentTime = SystemClock.uptimeMillis();
         KeyEvent anyKeyEvent = new KeyEvent(/* downTime= */ currentTime,
                 /* eventTime= */ currentTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HOME,
@@ -82,4 +93,3 @@
                 "Injecting KeyEvent requires INJECT_EVENTS permission");
     }
 }
-
diff --git a/tests/CarSecurityPermissionTest/src/com/android/car/telemetry/CarTelemetryManagerPermissionTest.java b/tests/CarSecurityPermissionTest/src/com/android/car/telemetry/CarTelemetryManagerPermissionTest.java
index 509c931..84409cf 100644
--- a/tests/CarSecurityPermissionTest/src/com/android/car/telemetry/CarTelemetryManagerPermissionTest.java
+++ b/tests/CarSecurityPermissionTest/src/com/android/car/telemetry/CarTelemetryManagerPermissionTest.java
@@ -22,7 +22,7 @@
 
 import android.car.Car;
 import android.car.telemetry.CarTelemetryManager;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.MetricsConfigKey;
 import android.content.Context;
 
 import androidx.annotation.NonNull;
@@ -45,8 +45,8 @@
 public class CarTelemetryManagerPermissionTest {
     private final Context mContext =
             InstrumentationRegistry.getInstrumentation().getTargetContext();
-    private final ManifestKey mManifestKey = new ManifestKey("name", 1);
-    private final byte[] mManifestBytes = "manifest".getBytes();
+    private final MetricsConfigKey mMetricsConfigKey = new MetricsConfigKey("name", 1);
+    private final byte[] mMetricsConfigBytes = "manifest".getBytes();
 
     private Car mCar;
     private CarTelemetryManager mCarTelemetryManager;
@@ -83,25 +83,26 @@
     }
 
     @Test
-    public void testAddManifest() throws Exception {
+    public void testAddMetricsConfig() throws Exception {
         Exception e = expectThrows(SecurityException.class,
-                () -> mCarTelemetryManager.addManifest(mManifestKey, mManifestBytes));
+                () -> mCarTelemetryManager.addMetricsConfig(mMetricsConfigKey,
+                        mMetricsConfigBytes));
 
         assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
     }
 
     @Test
-    public void testRemoveManifest() throws Exception {
+    public void testRemoveMetricsConfig() throws Exception {
         Exception e = expectThrows(SecurityException.class,
-                () -> mCarTelemetryManager.removeManifest(mManifestKey));
+                () -> mCarTelemetryManager.removeMetricsConfig(mMetricsConfigKey));
 
         assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
     }
 
     @Test
-    public void testRemoveAllManifests() throws Exception {
+    public void testRemoveAllMetricsConfigs() throws Exception {
         Exception e = expectThrows(SecurityException.class,
-                () -> mCarTelemetryManager.removeAllManifests());
+                () -> mCarTelemetryManager.removeAllMetricsConfigs());
 
         assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
     }
@@ -109,7 +110,7 @@
     @Test
     public void testSendFinishedReports() throws Exception {
         Exception e = expectThrows(SecurityException.class,
-                () -> mCarTelemetryManager.sendFinishedReports(mManifestKey));
+                () -> mCarTelemetryManager.sendFinishedReports(mMetricsConfigKey));
 
         assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
     }
@@ -122,23 +123,22 @@
         assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
     }
 
-    @Test
-    public void testSendScriptExecutionErrors() throws Exception {
-        Exception e = expectThrows(SecurityException.class,
-                () -> mCarTelemetryManager.sendScriptExecutionErrors());
-
-        assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
-    }
-
     private class FakeCarTelemetryResultsListener implements
             CarTelemetryManager.CarTelemetryResultsListener {
         @Override
-        public void onResult(@NonNull ManifestKey key, @NonNull byte[] result) {
+        public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
         }
 
         @Override
-        public void onError(@NonNull byte[] error) {
+        public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
+        }
+
+        @Override
+        public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key, int statusCode) {
+        }
+
+        @Override
+        public void onRemoveMetricsConfigStatus(@NonNull MetricsConfigKey key, boolean success) {
         }
     }
-
 }
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index af573f7..ea527fa 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -50,6 +50,8 @@
     <uses-permission android:name="android.car.permission.READ_CAR_STEERING"/>
     <uses-permission android:name="android.car.permission.STORAGE_MONITORING"/>
     <uses-permission android:name="android.car.permission.CAR_DYNAMICS_STATE"/>
+    <!-- use for CarServiceTest -->
+    <uses-permission android:name="android.car.permission.USE_CAR_TELEMETRY_SERVICE"/>
     <!-- Allow querying and writing to any property -->
     <uses-permission android:name="android.car.permission.CAR_ENERGY_PORTS" />
     <uses-permission android:name="android.car.permission.PERMISSION_CONTROL_ENERGY_PORTS" />
diff --git a/tests/NetworkPreferenceApp/res/layout/manager.xml b/tests/NetworkPreferenceApp/res/layout/manager.xml
index 0f826ee..a36bc2e 100644
--- a/tests/NetworkPreferenceApp/res/layout/manager.xml
+++ b/tests/NetworkPreferenceApp/res/layout/manager.xml
@@ -24,7 +24,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_weight="1"
-        android:weightSum="2">
+        android:weightSum="3">
         <LinearLayout
             style="@style/SectionContainer"
             android:layout_width="match_parent"
@@ -47,34 +47,29 @@
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:weightSum="2">
-            <LinearLayout
-                android:layout_width="match_parent"
+            <TextView
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:weightSum="2">
-                <TextView
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:text="@string/label_apply_latest_policy_on_boot"/>
-                <Switch
-                    android:id="@+id/reapplyPANSOnBootSwitch"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"/>
-            </LinearLayout>
-            <LinearLayout
-                android:layout_width="match_parent"
+                android:text="@string/label_apply_latest_policy_on_boot"/>
+            <Switch
+                android:id="@+id/reapplyPANSOnBootSwitch"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"/>
+        </LinearLayout>
+        <LinearLayout
+            style="@style/SectionContainer"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:weightSum="2">
+            <TextView
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:weightSum="2">
-                <TextView
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:text="@string/label_apply_wifi_policy_on_boot"/>
-                <Switch
-                    android:id="@+id/reapplyWifiSuggestionsOnBootSwitch"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"/>
-            </LinearLayout>
+                android:text="@string/label_apply_wifi_policy_on_boot"/>
+            <Switch
+                android:id="@+id/reapplyWifiSuggestionsOnBootSwitch"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"/>
         </LinearLayout>
     </LinearLayout>
     <LinearLayout
diff --git a/tests/ThemePlayground/AndroidManifest.xml b/tests/ThemePlayground/AndroidManifest.xml
index cebff5e..8070a15 100644
--- a/tests/ThemePlayground/AndroidManifest.xml
+++ b/tests/ThemePlayground/AndroidManifest.xml
@@ -48,6 +48,12 @@
              android:resizeableActivity="true"
              android:allowEmbedded="true">
         </activity>
+        <activity android:name=".ColorPalette"
+             android:label="@string/palette_elements"
+             android:windowSoftInputMode="stateUnchanged"
+             android:resizeableActivity="true"
+             android:allowEmbedded="true">
+        </activity>
         <activity android:name=".ProgressBarSamples"
                   android:label="@string/progress_bar_elements"
                   android:windowSoftInputMode="stateUnchanged"
diff --git a/tests/ThemePlayground/res/layout/color_palette.xml b/tests/ThemePlayground/res/layout/color_palette.xml
new file mode 100644
index 0000000..e335b30
--- /dev/null
+++ b/tests/ThemePlayground/res/layout/color_palette.xml
@@ -0,0 +1,436 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <ScrollView
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginBottom="8dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="8dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_0">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_0"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_10">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_10"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_50">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_50"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_100">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_100"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_200">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_200"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_300">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_300"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_400">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_400"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_500">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_500"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_600">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_600"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_700">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_700"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_800">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_800"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_900">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_900"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent1_1000">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent1_1000"/>
+            </FrameLayout>
+
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_0">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_0"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_10">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_10"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_50">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_50"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_100">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_100"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_200">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_200"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_300">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_300"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_400">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_400"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_500">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_500"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_600">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_600"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_700">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_700"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_800">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_800"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_900">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_900"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent2_1000">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent2_1000"/>
+            </FrameLayout>
+
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_0">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_0"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_10">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_10"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_50">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_50"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_100">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_100"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_200">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_200"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_300">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_300"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_400">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_400"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_500">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_500"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_600">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_600"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_700">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_700"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_800">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_800"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_900">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_900"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="100dp"
+                android:background="@drawable/system_accent3_1000">
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="*android:color/system_accent3_1000"/>
+            </FrameLayout>
+
+        </LinearLayout>
+    </ScrollView>
+    <include layout="@layout/menu_button"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/tests/ThemePlayground/res/menu/menu_main.xml b/tests/ThemePlayground/res/menu/menu_main.xml
index 8e4fd8c..647b02d 100644
--- a/tests/ThemePlayground/res/menu/menu_main.xml
+++ b/tests/ThemePlayground/res/menu/menu_main.xml
@@ -30,6 +30,10 @@
         android:orderInCategory="100"
         android:title="@string/panel_elements"/>
     <item
+        android:id="@+id/palette_elements"
+        android:orderInCategory="100"
+        android:title="@string/palette_elements"/>
+    <item
         android:id="@+id/progress_bar_elements"
         android:orderInCategory="100"
         android:title="@string/progress_bar_elements"/>
diff --git a/tests/ThemePlayground/res/values/drawables.xml b/tests/ThemePlayground/res/values/drawables.xml
new file mode 100644
index 0000000..aeb9c88
--- /dev/null
+++ b/tests/ThemePlayground/res/values/drawables.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <drawable name="system_accent1_0">@*android:color/system_accent1_0</drawable>
+    <drawable name="system_accent1_10">@*android:color/system_accent1_10</drawable>
+    <drawable name="system_accent1_50">@*android:color/system_accent1_50</drawable>
+    <drawable name="system_accent1_100">@*android:color/system_accent1_100</drawable>
+    <drawable name="system_accent1_200">@*android:color/system_accent1_200</drawable>
+    <drawable name="system_accent1_300">@*android:color/system_accent1_300</drawable>
+    <drawable name="system_accent1_400">@*android:color/system_accent1_400</drawable>
+    <drawable name="system_accent1_500">@*android:color/system_accent1_500</drawable>
+    <drawable name="system_accent1_600">@*android:color/system_accent1_600</drawable>
+    <drawable name="system_accent1_700">@*android:color/system_accent1_700</drawable>
+    <drawable name="system_accent1_800">@*android:color/system_accent1_800</drawable>
+    <drawable name="system_accent1_900">@*android:color/system_accent1_900</drawable>
+    <drawable name="system_accent1_1000">@*android:color/system_accent1_1000</drawable>
+
+    <drawable name="system_accent2_0">@*android:color/system_accent2_0</drawable>
+    <drawable name="system_accent2_10">@*android:color/system_accent2_10</drawable>
+    <drawable name="system_accent2_50">@*android:color/system_accent2_50</drawable>
+    <drawable name="system_accent2_100">@*android:color/system_accent2_100</drawable>
+    <drawable name="system_accent2_200">@*android:color/system_accent2_200</drawable>
+    <drawable name="system_accent2_300">@*android:color/system_accent2_300</drawable>
+    <drawable name="system_accent2_400">@*android:color/system_accent2_400</drawable>
+    <drawable name="system_accent2_500">@*android:color/system_accent2_500</drawable>
+    <drawable name="system_accent2_600">@*android:color/system_accent2_600</drawable>
+    <drawable name="system_accent2_700">@*android:color/system_accent2_700</drawable>
+    <drawable name="system_accent2_800">@*android:color/system_accent2_800</drawable>
+    <drawable name="system_accent2_900">@*android:color/system_accent2_900</drawable>
+    <drawable name="system_accent2_1000">@*android:color/system_accent2_1000</drawable>
+
+    <drawable name="system_accent3_0">@*android:color/system_accent3_0</drawable>
+    <drawable name="system_accent3_10">@*android:color/system_accent3_10</drawable>
+    <drawable name="system_accent3_50">@*android:color/system_accent3_50</drawable>
+    <drawable name="system_accent3_100">@*android:color/system_accent3_100</drawable>
+    <drawable name="system_accent3_200">@*android:color/system_accent3_200</drawable>
+    <drawable name="system_accent3_300">@*android:color/system_accent3_300</drawable>
+    <drawable name="system_accent3_400">@*android:color/system_accent3_400</drawable>
+    <drawable name="system_accent3_500">@*android:color/system_accent3_500</drawable>
+    <drawable name="system_accent3_600">@*android:color/system_accent3_600</drawable>
+    <drawable name="system_accent3_700">@*android:color/system_accent3_700</drawable>
+    <drawable name="system_accent3_800">@*android:color/system_accent3_800</drawable>
+    <drawable name="system_accent3_900">@*android:color/system_accent3_900</drawable>
+    <drawable name="system_accent3_1000">@*android:color/system_accent3_1000</drawable>
+
+</resources>
diff --git a/tests/ThemePlayground/res/values/strings.xml b/tests/ThemePlayground/res/values/strings.xml
index c355777..bd4b79a 100644
--- a/tests/ThemePlayground/res/values/strings.xml
+++ b/tests/ThemePlayground/res/values/strings.xml
@@ -20,6 +20,7 @@
     <string name="button_elements" translatable="false">Button Elements</string>
     <string name="progress_bar_elements" translatable="false">Progress Bar Elements</string>
     <string name="panel_elements" translatable="false">Color Panels</string>
+    <string name="palette_elements" translatable="false">Color Palette</string>
     <string name="dialog_elements" translatable="false">Dialogs</string>
     <string name="toggle_theme" translatable="false">Change configuration(Day/Night)</string>
     <string name="apply" translatable="false">Apply</string>
diff --git a/tests/ThemePlayground/src/com/android/car/themeplayground/AbstractSampleActivity.java b/tests/ThemePlayground/src/com/android/car/themeplayground/AbstractSampleActivity.java
index c197925..0f7cdfa 100644
--- a/tests/ThemePlayground/src/com/android/car/themeplayground/AbstractSampleActivity.java
+++ b/tests/ThemePlayground/src/com/android/car/themeplayground/AbstractSampleActivity.java
@@ -58,6 +58,8 @@
                 return startSampleActivity(ProgressBarSamples.class);
             case R.id.panel_elements:
                 return startSampleActivity(ColorSamples.class);
+            case R.id.palette_elements:
+                return startSampleActivity(ColorPalette.class);
             case R.id.dialog_elements:
                 return startSampleActivity(DialogSamples.class);
             case R.id.toggle_theme:
diff --git a/tests/ThemePlayground/src/com/android/car/themeplayground/ColorPalette.java b/tests/ThemePlayground/src/com/android/car/themeplayground/ColorPalette.java
new file mode 100644
index 0000000..5f9f9ac
--- /dev/null
+++ b/tests/ThemePlayground/src/com/android/car/themeplayground/ColorPalette.java
@@ -0,0 +1,32 @@
+/*
+ * 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.themeplayground;
+
+import android.os.Bundle;
+
+/**
+ * Activity that renders a bunch of color values from the theme.
+ */
+public class ColorPalette extends AbstractSampleActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Utils.onActivityCreateSetTheme(this);
+        setContentView(R.layout.color_palette);
+    }
+}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java
index 6a7ccdb..21aef7b 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java
@@ -26,6 +26,7 @@
 import android.car.CarBugreportManager;
 import android.car.CarBugreportManager.CarBugreportManagerCallback;
 import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.FlakyTest;
 import android.test.suitebuilder.annotation.LargeTest;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -71,7 +72,7 @@
     }
 
     @Test
-    public void test_requestBugreport_failsWhenNoPermission() throws Exception {
+    public void test_requestBugreport_failsWhenNoPermission() {
         dropPermissions();
 
         SecurityException expected =
@@ -83,6 +84,7 @@
     }
 
     @Test
+    @FlakyTest(bugId = 197652182)
     public void test_requestBugreport_works() throws Exception {
         getPermissions();
 
diff --git a/tests/carservice_test/src/com/android/car/CarTelemetryManagerTest.java b/tests/carservice_test/src/com/android/car/CarTelemetryManagerTest.java
new file mode 100644
index 0000000..88302de
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/CarTelemetryManagerTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2017 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;
+
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_ALREADY_EXISTS;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_NONE;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_PARSE_FAILED;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_VERSION_TOO_OLD;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.car.Car;
+import android.car.telemetry.CarTelemetryManager;
+import android.car.telemetry.MetricsConfigKey;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import com.android.car.telemetry.CarTelemetryService;
+import com.android.car.telemetry.TelemetryProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/** Test the public entry points for the CarTelemetryManager. */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CarTelemetryManagerTest extends MockedCarTestBase {
+    private static final long TIMEOUT_MS = 5_000L;
+    private static final String TAG = CarTelemetryManagerTest.class.getSimpleName();
+    private static final byte[] INVALID_METRICS_CONFIG = "bad config".getBytes();
+    private static final Executor DIRECT_EXECUTOR = Runnable::run;
+    private static final MetricsConfigKey KEY_V1 = new MetricsConfigKey("my_metrics_config", 1);
+    private static final MetricsConfigKey KEY_V2 = new MetricsConfigKey("my_metrics_config", 2);
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_V1 =
+            TelemetryProto.MetricsConfig.newBuilder()
+                    .setName("my_metrics_config").setVersion(1).setScript("no-op").build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_V2 =
+            METRICS_CONFIG_V1.toBuilder().setVersion(2).build();
+
+    private final FakeCarTelemetryResultsListener mListener = new FakeCarTelemetryResultsListener();
+    private final HandlerThread mTelemetryThread =
+            CarServiceUtils.getHandlerThread(CarTelemetryService.class.getSimpleName());
+    private final Handler mHandler = new Handler(mTelemetryThread.getLooper());
+
+    private CarTelemetryManager mCarTelemetryManager;
+    private CountDownLatch mIdleHandlerLatch = new CountDownLatch(1);
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        assumeTrue(getCar().isFeatureEnabled(Car.CAR_TELEMETRY_SERVICE));
+
+        mTelemetryThread.getLooper().getQueue().addIdleHandler(() -> {
+            mIdleHandlerLatch.countDown();
+            return true;
+        });
+
+        Log.i(TAG, "attempting to get CAR_TELEMETRY_SERVICE");
+        mCarTelemetryManager = (CarTelemetryManager) getCar().getCarManager(
+                Car.CAR_TELEMETRY_SERVICE);
+        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
+    }
+
+    @Test
+    public void testSetClearListener() {
+        mCarTelemetryManager.clearListener();
+        mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
+
+        // setListener multiple times should fail
+        assertThrows(IllegalStateException.class,
+                () -> mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener));
+    }
+
+    @Test
+    public void testAddMetricsConfig() throws Exception {
+        // invalid config, should fail
+        mCarTelemetryManager.addMetricsConfig(KEY_V1, INVALID_METRICS_CONFIG);
+        waitForHandlerThreadToFinish();
+        assertThat(mListener.getAddConfigStatus(KEY_V1)).isEqualTo(
+                ERROR_METRICS_CONFIG_PARSE_FAILED);
+
+        // new valid config, should succeed
+        mCarTelemetryManager.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+        waitForHandlerThreadToFinish();
+        assertThat(mListener.getAddConfigStatus(KEY_V1)).isEqualTo(ERROR_METRICS_CONFIG_NONE);
+
+        // duplicate config, should fail
+        mCarTelemetryManager.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+        waitForHandlerThreadToFinish();
+        assertThat(mListener.getAddConfigStatus(KEY_V1)).isEqualTo(
+                ERROR_METRICS_CONFIG_ALREADY_EXISTS);
+
+        // newer version of the config should replace older version
+        mCarTelemetryManager.addMetricsConfig(KEY_V2, METRICS_CONFIG_V2.toByteArray());
+        waitForHandlerThreadToFinish();
+        assertThat(mListener.getAddConfigStatus(KEY_V2)).isEqualTo(ERROR_METRICS_CONFIG_NONE);
+
+        // older version of the config should not be accepted
+        mCarTelemetryManager.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+        waitForHandlerThreadToFinish();
+        assertThat(mListener.getAddConfigStatus(KEY_V1)).isEqualTo(
+                ERROR_METRICS_CONFIG_VERSION_TOO_OLD);
+    }
+
+    @Test
+    public void testRemoveMetricsConfig() throws Exception {
+        mCarTelemetryManager.removeMetricsConfig(KEY_V1);
+        waitForHandlerThreadToFinish();
+        assertThat(mListener.getRemoveConfigStatus(KEY_V1)).isFalse();
+
+        mCarTelemetryManager.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+        waitForHandlerThreadToFinish();
+        mCarTelemetryManager.removeMetricsConfig(KEY_V1);
+        waitForHandlerThreadToFinish();
+        assertThat(mListener.getRemoveConfigStatus(KEY_V1)).isTrue();
+    }
+
+    private void waitForHandlerThreadToFinish() throws Exception {
+        assertWithMessage("handler not idle in %sms", TIMEOUT_MS)
+                .that(mIdleHandlerLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+        mIdleHandlerLatch = new CountDownLatch(1); // reset idle handler condition
+        mHandler.runWithScissors(() -> {
+        }, TIMEOUT_MS);
+    }
+
+
+    private static final class FakeCarTelemetryResultsListener
+            implements CarTelemetryManager.CarTelemetryResultsListener {
+
+        private Map<MetricsConfigKey, Boolean> mRemoveConfigStatusMap = new ArrayMap<>();
+        private Map<MetricsConfigKey, Integer> mAddConfigStatusMap = new ArrayMap<>();
+
+        @Override
+        public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
+        }
+
+        @Override
+        public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
+        }
+
+        @Override
+        public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key, int statusCode) {
+            mAddConfigStatusMap.put(key, statusCode);
+        }
+
+        @Override
+        public void onRemoveMetricsConfigStatus(@NonNull MetricsConfigKey key, boolean success) {
+            mRemoveConfigStatusMap.put(key, success);
+        }
+
+        public int getAddConfigStatus(MetricsConfigKey key) {
+            return mAddConfigStatusMap.getOrDefault(key, -100);
+        }
+
+        public boolean getRemoveConfigStatus(MetricsConfigKey key) {
+            return mRemoveConfigStatusMap.getOrDefault(key, false);
+        }
+    }
+}
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 aaca795..ba01d41 100644
--- a/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
@@ -87,13 +87,14 @@
     @Mock private IBinder mDaemonBinder;
     @Mock private IBinder mServiceBinder;
     @Mock private ICarWatchdog mCarWatchdogDaemon;
+    @Mock private WatchdogStorage mMockWatchdogStorage;
 
     private CarWatchdogService mCarWatchdogService;
     private ICarWatchdogServiceForSystem mWatchdogServiceForSystemImpl;
 
     @Before
     public void setUpMocks() throws Exception {
-        mCarWatchdogService = new CarWatchdogService(mMockContext);
+        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage);
 
         mockQueryService(CAR_WATCHDOG_DAEMON_INTERFACE, mDaemonBinder, mCarWatchdogDaemon);
         when(mCar.getEventHandler()).thenReturn(mMainHandler);
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/CarTelemetryServiceTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/CarTelemetryServiceTest.java
index 0e9a168..a17e3fa 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/CarTelemetryServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/CarTelemetryServiceTest.java
@@ -16,108 +16,167 @@
 
 package com.android.car.telemetry;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.car.telemetry.CarTelemetryManager;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.ICarTelemetryServiceListener;
+import android.car.telemetry.MetricsConfigKey;
 import android.content.Context;
+import android.os.Handler;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.car.CarLocalServices;
+import com.android.car.CarPropertyService;
+import com.android.car.systeminterface.SystemInterface;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoJUnitRunner;
 import org.mockito.junit.MockitoRule;
 
+import java.io.File;
+import java.nio.file.Files;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(MockitoJUnitRunner.class)
 @SmallTest
 public class CarTelemetryServiceTest {
+    private static final long TIMEOUT_MS = 5_000L;
+    private static final String METRICS_CONFIG_NAME = "my_metrics_config";
+    private static final MetricsConfigKey KEY_V1 = new MetricsConfigKey(METRICS_CONFIG_NAME, 1);
+    private static final MetricsConfigKey KEY_V2 = new MetricsConfigKey(METRICS_CONFIG_NAME, 2);
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_V1 =
+            TelemetryProto.MetricsConfig.newBuilder()
+                    .setName(METRICS_CONFIG_NAME).setVersion(1).setScript("no-op").build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_V2 =
+            TelemetryProto.MetricsConfig.newBuilder()
+                    .setName(METRICS_CONFIG_NAME).setVersion(2).setScript("no-op").build();
+
+    private CountDownLatch mIdleHandlerLatch = new CountDownLatch(1);
+    private CarTelemetryService mService;
+    private File mTempSystemCarDir;
+    private Handler mTelemetryHandler;
+
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
     @Mock
+    private CarPropertyService mMockCarPropertyService;
+    @Mock
     private Context mContext;
-
-    private final ManifestKey mManifestKeyV1 = new ManifestKey("Name", 1);
-    private final ManifestKey mManifestKeyV2 = new ManifestKey("Name", 2);
-    private final TelemetryProto.MetricsConfig mMetricsConfig =
-            TelemetryProto.MetricsConfig.newBuilder().setScript("no-op").build();
-
-    private CarTelemetryService mService;
+    @Mock
+    private ICarTelemetryServiceListener mMockListener;
+    @Mock
+    private SystemInterface mMockSystemInterface;
 
     @Before
-    public void setUp() {
-        mService = new CarTelemetryService(mContext);
+    public void setUp() throws Exception {
+        CarLocalServices.removeServiceForTest(SystemInterface.class);
+        CarLocalServices.addService(SystemInterface.class, mMockSystemInterface);
+
+        mTempSystemCarDir = Files.createTempDirectory("telemetry_test").toFile();
+        when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempSystemCarDir);
+
+        mService = new CarTelemetryService(mContext, mMockCarPropertyService);
+        mService.init();
+        mService.setListener(mMockListener);
+
+        mTelemetryHandler = mService.getTelemetryHandler();
+        mTelemetryHandler.getLooper().getQueue().addIdleHandler(() -> {
+            mIdleHandlerLatch.countDown();
+            return true;
+        });
+        waitForHandlerThreadToFinish();
     }
 
     @Test
-    public void testAddManifest_newManifest_shouldSucceed() {
-        int result = mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+    public void testAddMetricsConfig_newMetricsConfig_shouldSucceed() throws Exception {
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
 
-        assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NONE);
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onAddMetricsConfigStatus(
+                eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE));
     }
 
     @Test
-    public void testAddManifest_duplicateManifest_shouldFail() {
-        mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+    public void testAddMetricsConfig_duplicateMetricsConfig_shouldFail() throws Exception {
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onAddMetricsConfigStatus(
+                eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE));
 
-        int result = mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
 
-        assertThat(result).isEqualTo(CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS);
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onAddMetricsConfigStatus(
+                eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_ALREADY_EXISTS));
     }
 
     @Test
-    public void testAddManifest_invalidManifest_shouldFail() {
-        int result = mService.addManifest(mManifestKeyV1, "bad manifest".getBytes());
+    public void testAddMetricsConfig_invalidMetricsConfig_shouldFail() throws Exception {
+        mService.addMetricsConfig(KEY_V1, "bad manifest".getBytes());
 
-        assertThat(result).isEqualTo(CarTelemetryManager.ERROR_PARSE_MANIFEST_FAILED);
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onAddMetricsConfigStatus(
+                eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_PARSE_FAILED));
     }
 
     @Test
-    public void testAddManifest_olderManifest_shouldFail() {
-        mService.addManifest(mManifestKeyV2, mMetricsConfig.toByteArray());
+    public void testAddMetricsConfig_olderMetricsConfig_shouldFail() throws Exception {
+        mService.addMetricsConfig(KEY_V2, METRICS_CONFIG_V2.toByteArray());
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onAddMetricsConfigStatus(
+                eq(KEY_V2), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE));
 
-        int result = mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
 
-        assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NEWER_MANIFEST_EXISTS);
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onAddMetricsConfigStatus(
+                eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_VERSION_TOO_OLD));
     }
 
     @Test
-    public void testAddManifest_newerManifest_shouldReplace() {
-        mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+    public void testAddMetricsConfig_newerMetricsConfig_shouldReplace() throws Exception {
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
 
-        int result = mService.addManifest(mManifestKeyV2, mMetricsConfig.toByteArray());
+        mService.addMetricsConfig(KEY_V2, METRICS_CONFIG_V2.toByteArray());
 
-        assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NONE);
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onAddMetricsConfigStatus(
+                eq(KEY_V2), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE));
     }
 
     @Test
-    public void testRemoveManifest_manifestExists_shouldSucceed() {
-        mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+    public void testRemoveMetricsConfig_manifestExists_shouldSucceed() throws Exception {
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
 
-        boolean result = mService.removeManifest(mManifestKeyV1);
+        mService.removeMetricsConfig(KEY_V1);
 
-        assertThat(result).isTrue();
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onRemoveMetricsConfigStatus(eq(KEY_V1), eq(true));
     }
 
     @Test
-    public void testRemoveManifest_manifestDoesNotExist_shouldFail() {
-        boolean result = mService.removeManifest(mManifestKeyV1);
+    public void testRemoveMetricsConfig_manifestDoesNotExist_shouldFail() throws Exception {
+        mService.removeMetricsConfig(KEY_V1);
 
-        assertThat(result).isFalse();
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onRemoveMetricsConfigStatus(eq(KEY_V1), eq(false));
     }
 
-    @Test
-    public void testRemoveAllManifests_shouldSucceed() {
-        mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
-        mService.addManifest(mManifestKeyV2, mMetricsConfig.toByteArray());
-
-        mService.removeAllManifests();
-
-        // verify that the manifests are cleared by adding them again, should succeed
-        int result = mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
-        assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NONE);
-        result = mService.addManifest(mManifestKeyV2, mMetricsConfig.toByteArray());
-        assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NONE);
+    private void waitForHandlerThreadToFinish() throws Exception {
+        assertWithMessage("handler not idle in %sms", TIMEOUT_MS)
+                .that(mIdleHandlerLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+        mIdleHandlerLatch = new CountDownLatch(1); // reset idle handler condition
+        mTelemetryHandler.runWithScissors(() -> { }, TIMEOUT_MS);
     }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/MetricsConfigStoreTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/MetricsConfigStoreTest.java
new file mode 100644
index 0000000..5b326ff
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/MetricsConfigStoreTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.telemetry;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.car.telemetry.CarTelemetryManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class MetricsConfigStoreTest {
+    private static final String NAME_FOO = "Foo";
+    private static final String NAME_BAR = "Bar";
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_FOO =
+            TelemetryProto.MetricsConfig.newBuilder().setName(NAME_FOO).build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_BAR =
+            TelemetryProto.MetricsConfig.newBuilder().setName(NAME_BAR).build();
+
+    private File mTestRootDir;
+    private File mTestMetricsConfigDir;
+    private MetricsConfigStore mMetricsConfigStore;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestRootDir = Files.createTempDirectory("car_telemetry_test").toFile();
+        mTestMetricsConfigDir = new File(mTestRootDir, MetricsConfigStore.METRICS_CONFIG_DIR);
+
+        mMetricsConfigStore = new MetricsConfigStore(mTestRootDir);
+        assertThat(mTestMetricsConfigDir.exists()).isTrue();
+    }
+
+    @Test
+    public void testRetrieveActiveMetricsConfigs_shouldSendConfigsToListener() throws Exception {
+        writeConfigToDisk(METRICS_CONFIG_FOO);
+        writeConfigToDisk(METRICS_CONFIG_BAR);
+        mMetricsConfigStore = new MetricsConfigStore(mTestRootDir); // reload data
+
+        List<TelemetryProto.MetricsConfig> result = mMetricsConfigStore.getActiveMetricsConfigs();
+
+        assertThat(result).containsExactly(METRICS_CONFIG_FOO, METRICS_CONFIG_BAR);
+    }
+
+    @Test
+    public void testAddMetricsConfig_shouldWriteConfigToDisk() throws Exception {
+        int status = mMetricsConfigStore.addMetricsConfig(METRICS_CONFIG_FOO);
+
+        assertThat(status).isEqualTo(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE);
+        assertThat(readConfigFromFile(NAME_FOO)).isEqualTo(METRICS_CONFIG_FOO);
+    }
+
+    @Test
+    public void testDeleteMetricsConfig_whenNoConfig_shouldReturnFalse() {
+        boolean status = mMetricsConfigStore.deleteMetricsConfig(NAME_BAR);
+
+        assertThat(status).isFalse();
+    }
+
+    @Test
+    public void testDeleteMetricsConfig_shouldDeleteConfigFromDisk() throws Exception {
+        writeConfigToDisk(METRICS_CONFIG_BAR);
+
+        boolean status = mMetricsConfigStore.deleteMetricsConfig(NAME_BAR);
+
+        assertThat(status).isTrue();
+        assertThat(new File(mTestMetricsConfigDir, NAME_BAR).exists()).isFalse();
+    }
+
+    private void writeConfigToDisk(TelemetryProto.MetricsConfig config) throws Exception {
+        File file = new File(mTestMetricsConfigDir, config.getName());
+        Files.write(file.toPath(), config.toByteArray());
+        assertThat(file.exists()).isTrue();
+    }
+
+    private TelemetryProto.MetricsConfig readConfigFromFile(String fileName) throws Exception {
+        byte[] bytes = Files.readAllBytes(new File(mTestMetricsConfigDir, fileName).toPath());
+        return TelemetryProto.MetricsConfig.parseFrom(bytes);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/ResultStoreTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/ResultStoreTest.java
index d27aca4..af0281a 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/ResultStoreTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/ResultStoreTest.java
@@ -19,12 +19,9 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
-import android.os.Handler;
 import android.os.PersistableBundle;
 
 import org.junit.Before;
@@ -53,8 +50,6 @@
     private ResultStore mResultStore;
 
     @Mock
-    private Handler mMockHandler;
-    @Mock
     private ResultStore.FinalResultCallback mMockFinalResultCallback;
     @Captor
     private ArgumentCaptor<PersistableBundle> mBundleCaptor;
@@ -62,11 +57,6 @@
 
     @Before
     public void setUp() throws Exception {
-        // execute all handler posts immediately
-        when(mMockHandler.post(any())).thenAnswer(i -> {
-            ((Runnable) i.getArguments()[0]).run();
-            return true;
-        });
         TEST_INTERIM_BUNDLE.putString("test key", "interim value");
         TEST_FINAL_BUNDLE.putString("test key", "final value");
 
@@ -74,7 +64,7 @@
         mTestInterimResultDir = new File(mTestRootDir, ResultStore.INTERIM_RESULT_DIR);
         mTestFinalResultDir = new File(mTestRootDir, ResultStore.FINAL_RESULT_DIR);
 
-        mResultStore = new ResultStore(mMockHandler, mMockHandler, mTestRootDir);
+        mResultStore = new ResultStore(mTestRootDir);
     }
 
     @Test
@@ -89,7 +79,7 @@
         String testInterimFileName = "test_file_1";
         writeBundleToFile(mTestInterimResultDir, testInterimFileName, TEST_INTERIM_BUNDLE);
 
-        mResultStore = new ResultStore(mMockHandler, mMockHandler, mTestRootDir);
+        mResultStore = new ResultStore(mTestRootDir);
 
         // should compare value instead of reference
         assertThat(mResultStore.getInterimResult(testInterimFileName).toString())
@@ -221,7 +211,7 @@
         File fileBar = new File(mTestInterimResultDir, "bar");
         writeBundleToFile(fileFoo, TEST_INTERIM_BUNDLE);
         writeBundleToFile(fileBar, TEST_INTERIM_BUNDLE);
-        mResultStore = new ResultStore(mMockHandler, mMockHandler, mTestRootDir); // re-load data
+        mResultStore = new ResultStore(mTestRootDir); // re-load data
         PersistableBundle newData = new PersistableBundle();
         newData.putDouble("pi", 3.1415926);
 
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
similarity index 73%
rename from tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerUnitTest.java
rename to tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
index 02166ea..6c41e4b 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
@@ -30,7 +30,7 @@
 import android.car.hardware.CarPropertyConfig;
 import android.content.Context;
 import android.content.ServiceConnection;
-import android.os.Bundle;
+import android.content.SharedPreferences;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.PersistableBundle;
@@ -38,25 +38,27 @@
 import android.os.SystemClock;
 
 import com.android.car.CarPropertyService;
-import com.android.car.scriptexecutor.IScriptExecutor;
-import com.android.car.scriptexecutor.IScriptExecutorListener;
 import com.android.car.telemetry.ResultStore;
 import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.publisher.PublisherFactory;
+import com.android.car.telemetry.publisher.StatsManagerProxy;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(MockitoJUnitRunner.class)
-public class DataBrokerUnitTest {
+public class DataBrokerTest {
     private static final int PROP_ID = 100;
     private static final int PROP_AREA = 200;
     private static final int PRIORITY_HIGH = 1;
@@ -65,7 +67,6 @@
     private static final CarPropertyConfig<Integer> PROP_CONFIG =
             CarPropertyConfig.newBuilder(Integer.class, PROP_ID, PROP_AREA).setAccess(
                     CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ).build();
-    private static final Bundle DATA = new Bundle();
     private static final TelemetryProto.VehiclePropertyPublisher
             VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION =
             TelemetryProto.VehiclePropertyPublisher.newBuilder().setReadRate(
@@ -86,9 +87,12 @@
             TelemetryProto.MetricsConfig.newBuilder().setName("Bar").setVersion(
                     1).addSubscribers(SUBSCRIBER_BAR).build();
 
+
+    // when count reaches 0, all handler messages are scheduled to be dispatched after current time
+    private CountDownLatch mIdleHandlerLatch = new CountDownLatch(1);
+    private PersistableBundle mData = new PersistableBundle();
     private DataBrokerImpl mDataBroker;
     private FakeScriptExecutor mFakeScriptExecutor;
-    private Handler mHandler;
     private ScriptExecutionTask mHighPriorityTask;
     private ScriptExecutionTask mLowPriorityTask;
 
@@ -97,44 +101,53 @@
     @Mock
     private CarPropertyService mMockCarPropertyService;
     @Mock
+    private Handler mMockHandler;
+    @Mock
+    private StatsManagerProxy mMockStatsManager;
+    @Mock
+    private SharedPreferences mMockSharedPreferences;
+    @Mock
     private IBinder mMockScriptExecutorBinder;
     @Mock
     private ResultStore mMockResultStore;
 
-    @Captor
-    private ArgumentCaptor<ServiceConnection> mServiceConnectionCaptor;
-
     @Before
     public void setUp() {
         when(mMockCarPropertyService.getPropertyList())
                 .thenReturn(Collections.singletonList(PROP_CONFIG));
         // bind service should return true, otherwise broker is disabled
         when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
-        PublisherFactory factory = new PublisherFactory(mMockCarPropertyService);
+        PublisherFactory factory = new PublisherFactory(
+                mMockCarPropertyService, mMockHandler, mMockStatsManager, mMockSharedPreferences);
         mDataBroker = new DataBrokerImpl(mMockContext, factory, mMockResultStore);
-        mHandler = mDataBroker.getWorkerHandler();
+        // add IdleHandler to get notified when all messages and posts are handled
+        mDataBroker.getTelemetryHandler().getLooper().getQueue().addIdleHandler(() -> {
+            mIdleHandlerLatch.countDown();
+            return true;
+        });
 
         mFakeScriptExecutor = new FakeScriptExecutor();
         when(mMockScriptExecutorBinder.queryLocalInterface(anyString()))
                 .thenReturn(mFakeScriptExecutor);
-        // capture ServiceConnection and connect to fake ScriptExecutor
-        verify(mMockContext).bindServiceAsUser(
-                any(), mServiceConnectionCaptor.capture(), anyInt(), any());
-        mServiceConnectionCaptor.getValue().onServiceConnected(
-                null, mMockScriptExecutorBinder);
+        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(i -> {
+            ServiceConnection conn = i.getArgument(1);
+            conn.onServiceConnected(null, mMockScriptExecutorBinder);
+            return true;
+        });
 
         mHighPriorityTask = new ScriptExecutionTask(
                 new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
-                DATA,
+                mData,
                 SystemClock.elapsedRealtime());
         mLowPriorityTask = new ScriptExecutionTask(
                 new DataSubscriber(mDataBroker, METRICS_CONFIG_BAR, SUBSCRIBER_BAR),
-                DATA,
+                mData,
                 SystemClock.elapsedRealtime());
     }
 
     @Test
-    public void testSetTaskExecutionPriority_whenNoTask_shouldNotInvokeScriptExecutor() {
+    public void testSetTaskExecutionPriority_whenNoTask_shouldNotInvokeScriptExecutor()
+            throws Exception {
         mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
 
         waitForHandlerThreadToFinish();
@@ -142,7 +155,8 @@
     }
 
     @Test
-    public void testSetTaskExecutionPriority_whenNextTaskPriorityLow_shouldNotRunTask() {
+    public void testSetTaskExecutionPriority_whenNextTaskPriorityLow_shouldNotRunTask()
+            throws Exception {
         mDataBroker.getTaskQueue().add(mLowPriorityTask);
 
         mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
@@ -154,7 +168,8 @@
     }
 
     @Test
-    public void testSetTaskExecutionPriority_whenNextTaskPriorityHigh_shouldInvokeScriptExecutor() {
+    public void testSetTaskExecutionPriority_whenNextTaskPriorityHigh_shouldInvokeScriptExecutor()
+            throws Exception {
         mDataBroker.getTaskQueue().add(mHighPriorityTask);
 
         mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
@@ -166,7 +181,7 @@
     }
 
     @Test
-    public void testScheduleNextTask_whenNoTask_shouldNotInvokeScriptExecutor() {
+    public void testScheduleNextTask_whenNoTask_shouldNotInvokeScriptExecutor() throws Exception {
         mDataBroker.scheduleNextTask();
 
         waitForHandlerThreadToFinish();
@@ -174,7 +189,8 @@
     }
 
     @Test
-    public void testScheduleNextTask_whenTaskInProgress_shouldNotInvokeScriptExecutorAgain() {
+    public void testScheduleNextTask_whenTaskInProgress_shouldNotInvokeScriptExecutorAgain()
+            throws Exception {
         PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
         taskQueue.add(mHighPriorityTask);
         mDataBroker.scheduleNextTask(); // start a task
@@ -184,6 +200,7 @@
 
         mDataBroker.scheduleNextTask(); // schedule next task while the last task is in progress
 
+        waitForHandlerThreadToFinish();
         // verify task is not polled
         assertThat(taskQueue.peek()).isEqualTo(mHighPriorityTask);
         // expect one invocation for the task that is running
@@ -191,7 +208,8 @@
     }
 
     @Test
-    public void testScheduleNextTask_whenTaskCompletes_shouldAutomaticallyScheduleNextTask() {
+    public void testScheduleNextTask_whenTaskCompletes_shouldAutomaticallyScheduleNextTask()
+            throws Exception {
         PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
         // add two tasks into the queue for execution
         taskQueue.add(mHighPriorityTask);
@@ -200,7 +218,7 @@
         mDataBroker.scheduleNextTask(); // start a task
         waitForHandlerThreadToFinish();
         // end a task, should automatically schedule the next task
-        mFakeScriptExecutor.notifyScriptSuccess(DATA);
+        mFakeScriptExecutor.notifyScriptSuccess(mData); // posts to telemetry handler
 
         waitForHandlerThreadToFinish();
         // verify queue is empty, both tasks are polled and executed
@@ -209,30 +227,62 @@
     }
 
     @Test
-    public void testScheduleNextTask_onScriptSuccess_shouldStoreInterimResult() {
+    public void testScheduleNextTask_onScriptSuccess_shouldStoreInterimResult() throws Exception {
+        mData.putBoolean("script is finished", false);
+        mData.putDouble("value of euler's number", 2.71828);
         mDataBroker.getTaskQueue().add(mHighPriorityTask);
-        ArgumentCaptor<PersistableBundle> persistableBundleCaptor =
-                ArgumentCaptor.forClass(PersistableBundle.class);
 
         mDataBroker.scheduleNextTask();
         waitForHandlerThreadToFinish();
-        mFakeScriptExecutor.notifyScriptSuccess(new Bundle()); // pass in empty bundle
+        mFakeScriptExecutor.notifyScriptSuccess(mData); // posts to telemetry handler
 
+        waitForHandlerThreadToFinish();
         assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
-        verify(mMockResultStore).putInterimResult(eq(METRICS_CONFIG_FOO.getName()),
-                persistableBundleCaptor.capture());
-        assertThat(persistableBundleCaptor.getValue().toString()).isEqualTo(
-                new PersistableBundle().toString()); // expect empty persistable bundle
+        verify(mMockResultStore).putInterimResult(
+                eq(mHighPriorityTask.getMetricsConfig().getName()), eq(mData));
     }
 
     @Test
-    public void testScheduleNextTask_whenBindScriptExecutorFailed_shouldDisableBroker() {
+    public void testScheduleNextTask_whenScriptFinishes_shouldStoreFinalResult()
+            throws Exception {
+        mData.putBoolean("script is finished", true);
+        mData.putDouble("value of pi", 3.14159265359);
+        mDataBroker.getTaskQueue().add(mHighPriorityTask);
+
+        mDataBroker.scheduleNextTask();
+        waitForHandlerThreadToFinish();
+        mFakeScriptExecutor.notifyScriptFinish(mData); // posts to telemetry handler
+
+        waitForHandlerThreadToFinish();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+        verify(mMockResultStore).putFinalResult(
+                eq(mHighPriorityTask.getMetricsConfig().getName()), eq(mData));
+    }
+
+    @Test
+    public void testScheduleNextTask_whenInterimDataExists_shouldPassToScriptExecutor()
+            throws Exception {
+        mData.putDouble("value of golden ratio", 1.618033);
+        mDataBroker.getTaskQueue().add(mHighPriorityTask);
+        when(mMockResultStore.getInterimResult(mHighPriorityTask.getMetricsConfig().getName()))
+                .thenReturn(mData);
+
+        mDataBroker.scheduleNextTask();
+
+        waitForHandlerThreadToFinish();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+        assertThat(mFakeScriptExecutor.getSavedState()).isEqualTo(mData);
+    }
+
+    @Test
+    public void testScheduleNextTask_whenBindScriptExecutorFailed_shouldDisableBroker()
+            throws Exception {
+        // fail all future attempts to bind to it
+        Mockito.reset(mMockContext);
+        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(false);
         mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
         PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
         taskQueue.add(mHighPriorityTask);
-        // disconnect ScriptExecutor and fail all future attempts to bind to it
-        mServiceConnectionCaptor.getValue().onServiceDisconnected(null);
-        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(false);
 
         // will rebind to ScriptExecutor if it is null
         mDataBroker.scheduleNextTask();
@@ -244,7 +294,8 @@
     }
 
     @Test
-    public void testScheduleNextTask_whenScriptExecutorFails_shouldRequeueTask() {
+    public void testScheduleNextTask_whenScriptExecutorThrowsException_shouldRequeueTask()
+            throws Exception {
         PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
         taskQueue.add(mHighPriorityTask);
         mFakeScriptExecutor.failNextApiCalls(1); // fail the next invokeScript() call
@@ -258,7 +309,7 @@
     }
 
     @Test
-    public void testAddTaskToQueue_shouldInvokeScriptExecutor() {
+    public void testAddTaskToQueue_shouldInvokeScriptExecutor() throws Exception {
         mDataBroker.addTaskToQueue(mHighPriorityTask);
 
         waitForHandlerThreadToFinish();
@@ -269,7 +320,6 @@
     public void testAddMetricsConfiguration_newMetricsConfig() {
         mDataBroker.addMetricsConfiguration(METRICS_CONFIG_BAR);
 
-        waitForHandlerThreadToFinish();
         assertThat(mDataBroker.getSubscriptionMap()).hasSize(1);
         assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_BAR.getName());
         // there should be one data subscriber in the subscription list of METRICS_CONFIG_BAR
@@ -282,7 +332,6 @@
         // this metrics config has already been added in setUp()
         mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
 
-        waitForHandlerThreadToFinish();
         assertThat(mDataBroker.getSubscriptionMap()).hasSize(1);
         assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_FOO.getName());
         assertThat(mDataBroker.getSubscriptionMap().get(METRICS_CONFIG_FOO.getName())).hasSize(1);
@@ -294,7 +343,7 @@
         mDataBroker.addMetricsConfiguration(METRICS_CONFIG_BAR);
         ScriptExecutionTask taskWithMetricsConfigFoo = new ScriptExecutionTask(
                 new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
-                DATA,
+                mData,
                 SystemClock.elapsedRealtime());
         PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
         taskQueue.add(mHighPriorityTask); // associated with METRICS_CONFIG_FOO
@@ -304,7 +353,6 @@
 
         mDataBroker.removeMetricsConfiguration(METRICS_CONFIG_FOO);
 
-        waitForHandlerThreadToFinish();
         assertThat(taskQueue).hasSize(1);
         assertThat(taskQueue.poll()).isEqualTo(mLowPriorityTask);
     }
@@ -313,25 +361,28 @@
     public void testRemoveMetricsConfiguration_whenMetricsConfigNonExistent_shouldDoNothing() {
         mDataBroker.removeMetricsConfiguration(METRICS_CONFIG_BAR);
 
-        waitForHandlerThreadToFinish();
         assertThat(mDataBroker.getSubscriptionMap()).hasSize(0);
     }
 
-    private void waitForHandlerThreadToFinish() {
+    private void waitForHandlerThreadToFinish() throws Exception {
         assertWithMessage("handler not idle in %sms", TIMEOUT_MS)
-                .that(mHandler.runWithScissors(() -> {}, TIMEOUT_MS)).isTrue();
+                .that(mIdleHandlerLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+        mIdleHandlerLatch = new CountDownLatch(1); // reset idle handler condition
     }
 
     private static class FakeScriptExecutor implements IScriptExecutor {
         private IScriptExecutorListener mListener;
         private int mApiInvocationCount = 0;
         private int mFailApi = 0;
+        private PersistableBundle mSavedState = null;
 
         @Override
-        public void invokeScript(String scriptBody, String functionName, Bundle publishedData,
-                @Nullable Bundle savedState, IScriptExecutorListener listener)
+        public void invokeScript(String scriptBody, String functionName,
+                PersistableBundle publishedData, @Nullable PersistableBundle savedState,
+                IScriptExecutorListener listener)
                 throws RemoteException {
             mApiInvocationCount++;
+            mSavedState = savedState;
             mListener = listener;
             if (mFailApi > 0) {
                 mFailApi--;
@@ -345,7 +396,7 @@
         }
 
         /** Mocks script temporary completion. */
-        public void notifyScriptSuccess(Bundle bundle) {
+        public void notifyScriptSuccess(PersistableBundle bundle) {
             try {
                 mListener.onSuccess(bundle);
             } catch (RemoteException e) {
@@ -353,6 +404,15 @@
             }
         }
 
+        /** Mocks script producing final result. */
+        public void notifyScriptFinish(PersistableBundle bundle) {
+            try {
+                mListener.onScriptFinished(bundle);
+            } catch (RemoteException e) {
+                // nothing to do
+            }
+        }
+
         /** Fails the next N invokeScript() call. */
         public void failNextApiCalls(int n) {
             mFailApi = n;
@@ -362,5 +422,10 @@
         public int getApiInvocationCount() {
             return mApiInvocationCount;
         }
+
+        /** Returns the interim data passed in invokeScript(). */
+        public PersistableBundle getSavedState() {
+            return mSavedState;
+        }
     }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/CarTelemetrydPublisherTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/CarTelemetrydPublisherTest.java
index 221b6a0..bc3cd47 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/CarTelemetrydPublisherTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/CarTelemetrydPublisherTest.java
@@ -23,19 +23,21 @@
 
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.automotive.telemetry.internal.ICarDataListener;
 import android.automotive.telemetry.internal.ICarTelemetryInternal;
 import android.car.test.mocks.AbstractExtendedMockitoTestCase;
 import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
 import android.os.ServiceManager;
 
 import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.car.test.FakeHandlerWrapper;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -56,24 +58,26 @@
                             .setId(CAR_DATA_ID_1))
                     .build();
 
+    private final FakeHandlerWrapper mFakeHandlerWrapper =
+            new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.IMMEDIATE);
+
     @Mock private IBinder mMockBinder;
-    @Mock private ICarTelemetryInternal mMockCarTelemetryInternal;
     @Mock private DataSubscriber mMockDataSubscriber;
 
-    private final CarTelemetrydPublisher mPublisher = new CarTelemetrydPublisher();
+    @Captor private ArgumentCaptor<IBinder.DeathRecipient> mLinkToDeathCallbackCaptor;
 
-    // ICarTelemetryInternal side of the listener.
-    @Captor private ArgumentCaptor<ICarDataListener> mCarDataListenerCaptor;
+    @Nullable private Throwable mPublisherFailure;
+    private FakeCarTelemetryInternal mFakeCarTelemetryInternal;
+    private CarTelemetrydPublisher mPublisher;
 
     @Before
     public void setUp() throws Exception {
+        mPublisher = new CarTelemetrydPublisher(
+                this::onPublisherFailure, mFakeHandlerWrapper.getMockHandler());
+        mFakeCarTelemetryInternal = new FakeCarTelemetryInternal(mMockBinder);
         when(mMockDataSubscriber.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
-        doNothing().when(mMockCarTelemetryInternal).setListener(mCarDataListenerCaptor.capture());
-    }
-
-    private void mockICarTelemetryInternalBinder() {
-        when(mMockBinder.queryLocalInterface(any())).thenReturn(mMockCarTelemetryInternal);
-        when(mMockCarTelemetryInternal.asBinder()).thenReturn(mMockBinder);
+        when(mMockBinder.queryLocalInterface(any())).thenReturn(mFakeCarTelemetryInternal);
+        doNothing().when(mMockBinder).linkToDeath(mLinkToDeathCallbackCaptor.capture(), anyInt());
         doReturn(mMockBinder).when(() -> ServiceManager.checkService(SERVICE_NAME));
     }
 
@@ -84,18 +88,15 @@
 
     @Test
     public void testAddDataSubscriber_registersNewListener() {
-        mockICarTelemetryInternalBinder();
-
         mPublisher.addDataSubscriber(mMockDataSubscriber);
 
-        assertThat(mCarDataListenerCaptor.getAllValues()).hasSize(1);
+        assertThat(mFakeCarTelemetryInternal.mListener).isNotNull();
         assertThat(mPublisher.isConnectedToCarTelemetryd()).isTrue();
         assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
     }
 
     @Test
     public void testAddDataSubscriber_withInvalidId_fails() {
-        mockICarTelemetryInternalBinder();
         DataSubscriber invalidDataSubscriber = Mockito.mock(DataSubscriber.class);
         when(invalidDataSubscriber.getPublisherParam()).thenReturn(
                 TelemetryProto.Publisher.newBuilder()
@@ -107,7 +108,7 @@
                 () -> mPublisher.addDataSubscriber(invalidDataSubscriber));
 
         assertThat(error).hasMessageThat().contains("Invalid CarData ID");
-        assertThat(mCarDataListenerCaptor.getAllValues()).hasSize(0);
+        assertThat(mFakeCarTelemetryInternal.mListener).isNull();
         assertThat(mPublisher.isConnectedToCarTelemetryd()).isFalse();
         assertThat(mPublisher.hasDataSubscriber(invalidDataSubscriber)).isFalse();
     }
@@ -118,8 +119,7 @@
     }
 
     @Test
-    public void testRemoveDataSubscriber_removesOnlySingleSubscriber() throws Exception {
-        mockICarTelemetryInternalBinder();
+    public void testRemoveDataSubscriber_removesOnlySingleSubscriber() {
         DataSubscriber subscriber2 = Mockito.mock(DataSubscriber.class);
         when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
         mPublisher.addDataSubscriber(mMockDataSubscriber);
@@ -129,23 +129,21 @@
 
         assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
         assertThat(mPublisher.hasDataSubscriber(subscriber2)).isFalse();
-        verify(mMockCarTelemetryInternal, never()).clearListener();
+        assertThat(mFakeCarTelemetryInternal.mListener).isNotNull();
     }
 
     @Test
-    public void testRemoveDataSubscriber_disconnectsFromICarTelemetry() throws Exception {
-        mockICarTelemetryInternalBinder();
+    public void testRemoveDataSubscriber_disconnectsFromICarTelemetry() {
         mPublisher.addDataSubscriber(mMockDataSubscriber);
 
         mPublisher.removeDataSubscriber(mMockDataSubscriber);
 
         assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
-        verify(mMockCarTelemetryInternal, times(1)).clearListener();
+        assertThat(mFakeCarTelemetryInternal.mListener).isNull();
     }
 
     @Test
-    public void testRemoveAllDataSubscribers_succeeds() throws Exception {
-        mockICarTelemetryInternalBinder();
+    public void testRemoveAllDataSubscribers_succeeds() {
         DataSubscriber subscriber2 = Mockito.mock(DataSubscriber.class);
         when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
         mPublisher.addDataSubscriber(mMockDataSubscriber);
@@ -155,8 +153,68 @@
 
         assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
         assertThat(mPublisher.hasDataSubscriber(subscriber2)).isFalse();
-        verify(mMockCarTelemetryInternal, times(1)).clearListener();
+        assertThat(mFakeCarTelemetryInternal.mListener).isNull();
     }
 
-    // TODO(b/189142577): add test cases when connecting to cartelemetryd fails
+    @Test
+    public void testNotifiesFailureConsumer_whenBinderDies() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        mLinkToDeathCallbackCaptor.getValue().binderDied();
+
+        assertThat(mFakeCarTelemetryInternal.mSetListenerCallCount).isEqualTo(1);
+        assertThat(mPublisherFailure).hasMessageThat()
+                .contains("ICarTelemetryInternal binder died");
+    }
+
+    @Test
+    public void testNotifiesFailureConsumer_whenFailsConnectToService() {
+        mFakeCarTelemetryInternal.setApiFailure(new RemoteException("tough life"));
+
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        assertThat(mPublisherFailure).hasMessageThat()
+                .contains("Cannot set CarData listener");
+    }
+
+    private void onPublisherFailure(AbstractPublisher publisher, Throwable error) {
+        mPublisherFailure = error;
+    }
+
+    private static class FakeCarTelemetryInternal implements ICarTelemetryInternal {
+        @Nullable ICarDataListener mListener;
+        int mSetListenerCallCount = 0;
+        private final IBinder mBinder;
+        @Nullable private RemoteException mApiFailure = null;
+
+        FakeCarTelemetryInternal(IBinder binder) {
+            mBinder = binder;
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return mBinder;
+        }
+
+        @Override
+        public void setListener(ICarDataListener listener) throws RemoteException {
+            mSetListenerCallCount += 1;
+            if (mApiFailure != null) {
+                throw mApiFailure;
+            }
+            mListener = listener;
+        }
+
+        @Override
+        public void clearListener() throws RemoteException {
+            if (mApiFailure != null) {
+                throw mApiFailure;
+            }
+            mListener = null;
+        }
+
+        void setApiFailure(RemoteException e) {
+            mApiFailure = e;
+        }
+    }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
index 8c063aa..a672bd5 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
@@ -17,18 +17,39 @@
 package com.android.car.telemetry.publisher;
 
 import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.APP_START_MEMORY_STATE_CAPTURED;
+import static com.android.car.telemetry.publisher.StatsPublisher.APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID;
+import static com.android.car.telemetry.publisher.StatsPublisher.APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID;
+import static com.android.car.telemetry.publisher.StatsPublisher.ATOM_APP_START_MEMORY_STATE_CAPTURED_ID;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.StatsManager;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.SystemClock;
+
+import com.android.car.telemetry.StatsLogProto;
+import com.android.car.telemetry.StatsdConfigProto;
 import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.car.test.FakeHandlerWrapper;
 import com.android.car.test.FakeSharedPreferences;
 
+import com.google.common.collect.Range;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -51,26 +72,203 @@
                     .addSubscribers(SUBSCRIBER_1)
                     .build();
 
+    private static final long SUBSCRIBER_1_HASH = -8101507323446050791L;  // Used as ID.
+
+    private static final StatsdConfigProto.StatsdConfig STATSD_CONFIG_1 =
+            StatsdConfigProto.StatsdConfig.newBuilder()
+                    .setId(SUBSCRIBER_1_HASH)
+                    .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+                            .setId(APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID)
+                            .setSimpleAtomMatcher(
+                                    StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+                                            .setAtomId(
+                                                    ATOM_APP_START_MEMORY_STATE_CAPTURED_ID)))
+                    .addEventMetric(StatsdConfigProto.EventMetric.newBuilder()
+                            .setId(APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID)
+                            .setWhat(APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID))
+                    .addAllowedLogSource("AID_SYSTEM")
+                    .build();
+
+    private static final StatsLogProto.ConfigMetricsReportList EMPTY_STATS_REPORT =
+            StatsLogProto.ConfigMetricsReportList.newBuilder().build();
+
+    private StatsPublisher mPublisher;  // subject
+    private final FakeSharedPreferences mFakeSharedPref = new FakeSharedPreferences();
+    private final FakeHandlerWrapper mFakeHandlerWrapper =
+            new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.QUEUEING);
+    private Throwable mPublisherFailure;
+
     @Mock private DataSubscriber mMockDataSubscriber;
     @Mock private StatsManagerProxy mStatsManager;
-    private final FakeSharedPreferences mFakeSharedPref = new FakeSharedPreferences();
 
-    private StatsPublisher mPublisher;
+    @Captor private ArgumentCaptor<PersistableBundle> mBundleCaptor;
 
     @Before
     public void setUp() throws Exception {
-        mPublisher = new StatsPublisher(mStatsManager, mFakeSharedPref);
+        mPublisher = createRestartedPublisher();
         when(mMockDataSubscriber.getPublisherParam()).thenReturn(STATS_PUBLISHER_PARAMS_1);
         when(mMockDataSubscriber.getMetricsConfig()).thenReturn(METRICS_CONFIG);
         when(mMockDataSubscriber.getSubscriber()).thenReturn(SUBSCRIBER_1);
     }
 
+    /**
+     * Emulates a restart by creating a new StatsPublisher. StatsManager and SharedPreference
+     * stays the same.
+     */
+    private StatsPublisher createRestartedPublisher() {
+        return new StatsPublisher(
+                this::onPublisherFailure,
+                mStatsManager,
+                mFakeSharedPref,
+                mFakeHandlerWrapper.getMockHandler());
+    }
+
     @Test
-    public void testAddDataSubscriber_registersNewListener() {
+    public void testAddDataSubscriber_registersNewListener() throws Exception {
         mPublisher.addDataSubscriber(mMockDataSubscriber);
 
+        verify(mStatsManager, times(1))
+                .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
         assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
     }
 
-    // TODO(b/189142577): add test cases when connecting to Statsd fails
+    @Test
+    public void testAddDataSubscriber_sameVersion_addsToStatsdOnce() throws Exception {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        verify(mStatsManager, times(1))
+                .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
+        assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+    }
+
+    @Test
+    public void testAddDataSubscriber_whenRestarted_addsToStatsdOnce() throws Exception {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        StatsPublisher publisher2 = createRestartedPublisher();
+
+        publisher2.addDataSubscriber(mMockDataSubscriber);
+
+        verify(mStatsManager, times(1))
+                .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
+        assertThat(publisher2.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_removesFromStatsd() throws Exception {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        mPublisher.removeDataSubscriber(mMockDataSubscriber);
+
+        verify(mStatsManager, times(1)).removeConfig(SUBSCRIBER_1_HASH);
+        assertThat(mFakeSharedPref.getAll().isEmpty()).isTrue();  // also removes from SharedPref.
+        assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_ifNotFound_nothingHappensButCallsStatsdRemove()
+            throws Exception {
+        mPublisher.removeDataSubscriber(mMockDataSubscriber);
+
+        // It should try removing StatsdConfig from StatsD, in case it was added there before and
+        // left dangled.
+        verify(mStatsManager, times(1)).removeConfig(SUBSCRIBER_1_HASH);
+        assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+    }
+
+    @Test
+    public void testRemoveAllDataSubscriber_whenRestarted_removesFromStatsdAndClears()
+            throws Exception {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        StatsPublisher publisher2 = createRestartedPublisher();
+
+        publisher2.removeAllDataSubscribers();
+
+        verify(mStatsManager, times(1)).removeConfig(SUBSCRIBER_1_HASH);
+        assertThat(mFakeSharedPref.getAll().isEmpty()).isTrue();  // also removes from SharedPref.
+        assertThat(publisher2.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+    }
+
+    @Test
+    public void testAddDataSubscriber_queuesPeriodicTaskInTheHandler() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).hasSize(1);
+        Message msg = mFakeHandlerWrapper.getQueuedMessages().get(0);
+        long expectedPullPeriodMillis = 10 * 60 * 1000;  // 10 minutes
+        assertThatMessageIsScheduledWithGivenDelay(msg, expectedPullPeriodMillis);
+    }
+
+    @Test
+    public void testAddDataSubscriber_whenFails_notifiesFailureConsumer() throws Exception {
+        doThrow(new StatsManager.StatsUnavailableException("fail"))
+                .when(mStatsManager).addConfig(anyLong(), any());
+
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        assertThat(mPublisherFailure).hasMessageThat().contains("Failed to add config");
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_removesPeriodicStatsdReportPull() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        mPublisher.removeDataSubscriber(mMockDataSubscriber);
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).isEmpty();
+    }
+
+    @Test
+    public void testRemoveAllDataSubscriber_removesPeriodicStatsdReportPull() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        mPublisher.removeAllDataSubscribers();
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).isEmpty();
+    }
+
+    @Test
+    public void testAfterDispatchItSchedulesANewPullReportTask() throws Exception {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        Message firstMessage = mFakeHandlerWrapper.getQueuedMessages().get(0);
+        when(mStatsManager.getReports(anyLong())).thenReturn(EMPTY_STATS_REPORT.toByteArray());
+
+        mFakeHandlerWrapper.dispatchQueuedMessages();
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).hasSize(1);
+        Message newMessage = mFakeHandlerWrapper.getQueuedMessages().get(0);
+        assertThat(newMessage).isNotEqualTo(firstMessage);
+        long expectedPullPeriodMillis = 10 * 60 * 1000;  // 10 minutes
+        assertThatMessageIsScheduledWithGivenDelay(newMessage, expectedPullPeriodMillis);
+    }
+
+    @Test
+    public void testPullsStatsdReport() throws Exception {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        when(mStatsManager.getReports(anyLong())).thenReturn(
+                StatsLogProto.ConfigMetricsReportList.newBuilder()
+                        // add 2 empty reports
+                        .addReports(StatsLogProto.ConfigMetricsReport.newBuilder())
+                        .addReports(StatsLogProto.ConfigMetricsReport.newBuilder())
+                        .build().toByteArray());
+
+        mFakeHandlerWrapper.dispatchQueuedMessages();
+
+        verify(mMockDataSubscriber).push(mBundleCaptor.capture());
+        assertThat(mBundleCaptor.getValue().getInt("reportsCount")).isEqualTo(2);
+    }
+
+    // TODO(b/189143813): add test cases when connecting to Statsd fails
+    // TODO(b/189143813): add test cases for handling config version upgrades
+
+    private void onPublisherFailure(AbstractPublisher publisher, Throwable error) {
+        mPublisherFailure = error;
+    }
+
+    private static void assertThatMessageIsScheduledWithGivenDelay(Message msg, long delayMillis) {
+        long expectedTimeMillis = SystemClock.uptimeMillis() + delayMillis;
+        long deltaMillis = 1000;  // +/- 1 seconds is good enough for testing
+        assertThat(msg.getWhen()).isIn(Range
+                .closed(expectedTimeMillis - deltaMillis, expectedTimeMillis + deltaMillis));
+    }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/VehiclePropertyPublisherTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/VehiclePropertyPublisherTest.java
index 2fafbce..508120f 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/VehiclePropertyPublisherTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/VehiclePropertyPublisherTest.java
@@ -35,11 +35,13 @@
 import android.car.hardware.CarPropertyValue;
 import android.car.hardware.property.CarPropertyEvent;
 import android.car.hardware.property.ICarPropertyEventListener;
-import android.os.Bundle;
+import android.os.Looper;
+import android.os.PersistableBundle;
 
 import com.android.car.CarPropertyService;
 import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.car.test.FakeHandlerWrapper;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -83,16 +85,18 @@
             CarPropertyConfig.newBuilder(Integer.class, PROP_ID_2, AREA_ID).setAccess(
                     CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_WRITE).build();
 
+    private final FakeHandlerWrapper mFakeHandlerWrapper =
+            new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.IMMEDIATE);
+
     @Mock
     private DataSubscriber mMockDataSubscriber;
-
     @Mock
     private CarPropertyService mMockCarPropertyService;
 
     @Captor
     private ArgumentCaptor<ICarPropertyEventListener> mCarPropertyCallbackCaptor;
     @Captor
-    private ArgumentCaptor<Bundle> mBundleCaptor;
+    private ArgumentCaptor<PersistableBundle> mBundleCaptor;
 
     private VehiclePropertyPublisher mVehiclePropertyPublisher;
 
@@ -101,7 +105,10 @@
         when(mMockDataSubscriber.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
         when(mMockCarPropertyService.getPropertyList())
                 .thenReturn(List.of(PROP_CONFIG_1, PROP_CONFIG_2_WRITE_ONLY));
-        mVehiclePropertyPublisher = new VehiclePropertyPublisher(mMockCarPropertyService);
+        mVehiclePropertyPublisher = new VehiclePropertyPublisher(
+                mMockCarPropertyService,
+                this::onPublisherFailure,
+                mFakeHandlerWrapper.getMockHandler());
     }
 
     @Test
@@ -193,8 +200,9 @@
         mCarPropertyCallbackCaptor.getValue().onEvent(Collections.singletonList(PROP_EVENT_1));
 
         verify(mMockDataSubscriber).push(mBundleCaptor.capture());
-        CarPropertyEvent event = mBundleCaptor.getValue().getParcelable(
-                VehiclePropertyPublisher.CAR_PROPERTY_EVENT_KEY);
-        assertThat(event).isEqualTo(PROP_EVENT_1);
+        // TODO(b/197269115): add more assertions on the contents of
+        // PersistableBundle object.
     }
+
+    private void onPublisherFailure(AbstractPublisher publisher, Throwable error) { }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorUnitTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorUnitTest.java
index a16ea1e..09591be 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorUnitTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -67,9 +68,16 @@
     public void setup() {
         when(mMockContext.getSystemService(anyString())).thenReturn(mMockActivityManager);
         when(mMockHandler.post(any(Runnable.class))).thenAnswer(i -> {
-            ((Runnable) i.getArguments()[0]).run();
+            Runnable runnable = i.getArgument(0);
+            runnable.run();
             return true;
         });
+        doAnswer(i -> {
+            MemoryInfo mi = i.getArgument(0);
+            mi.availMem = TEST_AVAILMEM;
+            mi.totalMem = TEST_TOTALMEM;
+            return null;
+        }).when(mMockActivityManager).getMemoryInfo(any(MemoryInfo.class));
     }
 
     @Test
@@ -142,13 +150,6 @@
     public void testAfterSetCallback_callbackCalled() throws IOException {
         SystemMonitor systemMonitor = new SystemMonitor(
                 mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG));
-        doAnswer(i -> {
-            Object[] args = i.getArguments();
-            MemoryInfo mi = (MemoryInfo) args[0];
-            mi.availMem = TEST_AVAILMEM;
-            mi.totalMem = TEST_TOTALMEM;
-            return null;
-        }).when(mMockActivityManager).getMemoryInfo(any(MemoryInfo.class));
 
         systemMonitor.setSystemMonitorCallback(mMockCallback);
 
@@ -180,6 +181,21 @@
         assertThat(systemMonitor.getCpuLoad()).isNull();
     }
 
+    @Test
+    public void testWhenUnsetCallback_sameCallbackFromSetCallbackIsRemoved() throws IOException {
+        SystemMonitor systemMonitor = new SystemMonitor(
+                mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG));
+
+        systemMonitor.setSystemMonitorCallback(mMockCallback);
+        systemMonitor.unsetSystemMonitorCallback();
+
+        verify(mMockHandler, times(1)).post(mRunnableCaptor.capture());
+        Runnable setRunnable = mRunnableCaptor.getValue();
+        verify(mMockHandler, times(1)).removeCallbacks(mRunnableCaptor.capture());
+        Runnable unsetRunnalbe = mRunnableCaptor.getValue();
+        assertThat(setRunnable).isEqualTo(unsetRunnalbe);
+    }
+
     /**
      * Creates and writes to the temp file, returns its path.
      */
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 1189784..f97965e 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
@@ -21,10 +21,11 @@
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetAliveUsers;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetAllUsers;
 import static android.car.watchdog.CarWatchdogManager.TIMEOUT_CRITICAL;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
 
+import static com.android.car.watchdog.WatchdogStorage.ZONE_OFFSET;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -62,6 +63,8 @@
 import android.automotive.watchdog.internal.ResourceSpecificConfiguration;
 import android.automotive.watchdog.internal.StateType;
 import android.automotive.watchdog.internal.UidType;
+import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
+import android.car.hardware.power.ICarPowerStateListener;
 import android.car.test.mocks.AbstractExtendedMockitoTestCase;
 import android.car.watchdog.CarWatchdogManager;
 import android.car.watchdog.ICarWatchdogServiceCallback;
@@ -93,10 +96,13 @@
 import android.util.ArraySet;
 import android.util.SparseArray;
 
+import com.android.car.CarLocalServices;
 import com.android.car.CarServiceUtils;
+import com.android.car.power.CarPowerManagementService;
 
 import com.google.common.truth.Correspondence;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -105,6 +111,9 @@
 import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -120,51 +129,92 @@
  * <p>This class contains unit tests for the {@link CarWatchdogService}.
  */
 @RunWith(MockitoJUnitRunner.class)
-public class CarWatchdogServiceUnitTest extends AbstractExtendedMockitoTestCase {
+public final class CarWatchdogServiceUnitTest extends AbstractExtendedMockitoTestCase {
     private static final String CAR_WATCHDOG_DAEMON_INTERFACE = "carwatchdogd_system";
     private static final int MAX_WAIT_TIME_MS = 3000;
     private static final int INVALID_SESSION_ID = -1;
+    private static final int RESOURCE_OVERUSE_KILLING_DELAY_MILLS = 1000;
+    private static final long STATS_DURATION_SECONDS = 3 * 60 * 60;
 
     @Mock private Context mMockContext;
     @Mock private PackageManager mMockPackageManager;
     @Mock private UserManager mMockUserManager;
+    @Mock private CarPowerManagementService mMockCarPowerManagementService;
     @Mock private IBinder mMockBinder;
     @Mock private ICarWatchdog mMockCarWatchdogDaemon;
+    @Mock private WatchdogStorage mMockWatchdogStorage;
 
     private CarWatchdogService mCarWatchdogService;
     private ICarWatchdogServiceForSystem mWatchdogServiceForSystemImpl;
     private IBinder.DeathRecipient mCarWatchdogDaemonBinderDeathRecipient;
     private BroadcastReceiver mBroadcastReceiver;
+    private boolean mIsDaemonCrashed;
+    private ICarPowerStateListener mCarPowerStateListener;
+    private TimeSourceInterface mTimeSource;
+
     private final SparseArray<String> mGenericPackageNameByUid = new SparseArray<>();
     private final SparseArray<List<String>> mPackagesBySharedUid = new SparseArray<>();
     private final ArrayMap<String, android.content.pm.PackageInfo> mPmPackageInfoByUserPackage =
             new ArrayMap<>();
+    private final ArraySet<String> mDisabledUserPackages = new ArraySet<>();
+    private final List<WatchdogStorage.UserPackageSettingsEntry> mUserPackageSettingsEntries =
+            new ArrayList<>();
+    private final List<WatchdogStorage.IoUsageStatsEntry> mIoUsageStatsEntries = new ArrayList<>();
 
     @Override
     protected void onSessionBuilder(CustomMockitoSessionBuilder builder) {
         builder
             .spyStatic(ServiceManager.class)
             .spyStatic(Binder.class)
-            .spyStatic(ActivityThread.class);
+            .spyStatic(ActivityThread.class)
+            .spyStatic(CarLocalServices.class);
     }
 
     /**
      * Initialize all of the objects with the @Mock annotation.
      */
     @Before
-    public void setUpMocks() throws Exception {
+    public void setUp() throws Exception {
         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockContext.getPackageName()).thenReturn(
                 CarWatchdogServiceUnitTest.class.getCanonicalName());
-        mCarWatchdogService = new CarWatchdogService(mMockContext);
+        doReturn(mMockCarPowerManagementService)
+                .when(() -> CarLocalServices.getService(CarPowerManagementService.class));
+        mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage);
+        mCarWatchdogService.setResourceOveruseKillingDelay(RESOURCE_OVERUSE_KILLING_DELAY_MILLS);
+        setDate(/* numDaysAgo= */ 0);
         mockWatchdogDaemon();
+        mockWatchdogStorage();
         setupUsers();
         mCarWatchdogService.init();
-        verifyResourceOveruseConfigurationsSynced(1);
-        mWatchdogServiceForSystemImpl = registerCarWatchdogService();
+        captureCarPowerStateListener();
         captureBroadcastReceiver();
+        captureWatchdogServiceForSystem();
         captureDaemonBinderDeathRecipient();
+        verifyDatabaseInit(/* wantedInvocations= */ 1);
         mockPackageManager();
+        verifyResourceOveruseConfigurationsSynced(/* wantedInvocations= */ 1);
+    }
+
+    /**
+     * Releases resources.
+     */
+    @After
+    public void tearDown() throws Exception {
+        if (mIsDaemonCrashed) {
+            /* Note: On daemon crash, CarWatchdogService retries daemon connection on the main
+             * thread. This retry outlives the test and impacts other test runs. Thus always call
+             * restartWatchdogDaemonAndAwait after crashing the daemon and before completing
+             * teardown.
+             */
+            restartWatchdogDaemonAndAwait();
+        }
+        mUserPackageSettingsEntries.clear();
+        mIoUsageStatsEntries.clear();
+        mGenericPackageNameByUid.clear();
+        mPackagesBySharedUid.clear();
+        mPmPackageInfoByUserPackage.clear();
+        mDisabledUserPackages.clear();
     }
 
     @Test
@@ -219,6 +269,7 @@
         verify(mMockCarWatchdogDaemon)
                 .notifySystemStateChange(
                         eq(StateType.GARAGE_MODE), eq(GarageMode.GARAGE_MODE_ON), eq(-1));
+        verify(mMockWatchdogStorage).shrinkDatabase();
     }
 
     @Test
@@ -228,6 +279,7 @@
         verify(mMockCarWatchdogDaemon)
                 .notifySystemStateChange(
                         eq(StateType.GARAGE_MODE), eq(GarageMode.GARAGE_MODE_OFF), eq(-1));
+        verify(mMockWatchdogStorage, never()).shrinkDatabase();
     }
 
     @Test
@@ -239,21 +291,110 @@
 
         SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
                 injectIoOveruseStatsForPackages(
-                        mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(),
-                        /* shouldNotifyPackages= */new ArraySet<>());
+                        mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                        /* shouldNotifyPackages= */ new ArraySet<>());
 
         ResourceOveruseStats expectedStats =
-                constructResourceOveruseStats(
-                        uid, mMockContext.getPackageName(),
+                constructResourceOveruseStats(uid, mMockContext.getPackageName(), 0,
                         packageIoOveruseStatsByUid.get(uid).ioOveruseStats);
 
         ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
                 CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
 
-        assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
-                + actualStats.toString())
-                .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
+    }
+
+    @Test
+    public void testGetResourceOveruseStatsForPast7days() throws Exception {
+        int uid = Binder.getCallingUid();
+        String packageName = mMockContext.getPackageName();
+        injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+                packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
+
+        long startTime = mTimeSource.now().atZone(ZONE_OFFSET).minusDays(4).toEpochSecond();
+        long duration = mTimeSource.now().getEpochSecond() - startTime;
+        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
+                UserHandle.getUserId(uid), packageName, 6))
+                .thenReturn(new IoOveruseStats.Builder(startTime, duration).setTotalOveruses(5)
+                        .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build());
+
+        injectIoOveruseStatsForPackages(mGenericPackageNameByUid,
+                /* killablePackages= */ Collections.singleton(packageName),
+                /* shouldNotifyPackages= */ new ArraySet<>());
+
+        ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+        IoOveruseStats ioOveruseStats =
+                new IoOveruseStats.Builder(startTime, duration + STATS_DURATION_SECONDS)
+                        .setKillableOnOveruse(true).setTotalOveruses(7).setTotalBytesWritten(24_600)
+                        .setTotalTimesKilled(2)
+                        .setRemainingWriteBytes(new PerStateBytes(20, 20, 20)).build();
+
+        ResourceOveruseStats expectedStats =
+                new ResourceOveruseStats.Builder(packageName, UserHandle.getUserHandleForUid(uid))
+                        .setIoOveruseStats(ioOveruseStats).build();
+
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+    }
+
+    @Test
+    public void testGetResourceOveruseStatsForPast7daysWithNoHistory() throws Exception {
+        int uid = Binder.getCallingUid();
+        String packageName = mMockContext.getPackageName();
+        injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+                packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
+
+        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
+                UserHandle.getUserId(uid), packageName, 6)).thenReturn(null);
+
+        injectIoOveruseStatsForPackages(mGenericPackageNameByUid,
+                /* killablePackages= */ Collections.singleton(packageName),
+                /* shouldNotifyPackages= */ new ArraySet<>());
+
+        ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+        ResourceOveruseStats expectedStats =
+                new ResourceOveruseStats.Builder(packageName, UserHandle.getUserHandleForUid(uid))
+                        .setIoOveruseStats(new IoOveruseStats.Builder(
+                                mTimeSource.now().getEpochSecond(), STATS_DURATION_SECONDS)
+                                .setKillableOnOveruse(true).setTotalOveruses(2)
+                                .setTotalBytesWritten(600)
+                                .setRemainingWriteBytes(new PerStateBytes(20, 20, 20)).build())
+                        .build();
+
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+    }
+
+    @Test
+    public void testGetResourceOveruseStatsForPast7daysWithNoCurrentStats() throws Exception {
+        int uid = Binder.getCallingUid();
+        String packageName = mMockContext.getPackageName();
+        injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+                packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
+
+        long startTime = mTimeSource.now().atZone(ZONE_OFFSET).minusDays(4).toEpochSecond();
+        long duration = mTimeSource.now().getEpochSecond() - startTime;
+        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
+                UserHandle.getUserId(uid), packageName, 6))
+                .thenReturn(new IoOveruseStats.Builder(startTime, duration).setTotalOveruses(5)
+                        .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build());
+
+        ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+        ResourceOveruseStats expectedStats =
+                new ResourceOveruseStats.Builder(packageName, UserHandle.getUserHandleForUid(uid))
+                .build();
+
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
     }
 
     @Test
@@ -266,31 +407,29 @@
 
         SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
                 injectIoOveruseStatsForPackages(
-                        mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(),
-                        /* shouldNotifyPackages= */new ArraySet<>());
+                        mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                        /* shouldNotifyPackages= */ new ArraySet<>());
 
         ResourceOveruseStats expectedStats =
-                constructResourceOveruseStats(sharedUid, "shared:system_shared_package",
+                constructResourceOveruseStats(sharedUid, "shared:system_shared_package", 0,
                         packageIoOveruseStatsByUid.get(sharedUid).ioOveruseStats);
 
         ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
                 CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
 
-        assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
-                + actualStats.toString())
-                .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
     }
 
     @Test
     public void testFailsGetResourceOveruseStatsWithInvalidArgs() throws Exception {
         assertThrows(IllegalArgumentException.class,
-                () -> mCarWatchdogService.getResourceOveruseStats(/* resourceOveruseFlag= */0,
+                () -> mCarWatchdogService.getResourceOveruseStats(/* resourceOveruseFlag= */ 0,
                         CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
 
         assertThrows(IllegalArgumentException.class,
                 () -> mCarWatchdogService.getResourceOveruseStats(
-                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* maxStatsPeriod= */0));
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* maxStatsPeriod= */ 0));
     }
 
     @Test
@@ -301,31 +440,97 @@
 
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
                 constructPackageIoOveruseStats(1103456,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
+                        /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
                 constructPackageIoOveruseStats(1201278,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
-                                /* writtenBytes= */constructPerStateBytes(5000, 6000, 9000),
-                                /* totalOveruses= */2)));
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+                                /* writtenBytes= */ constructPerStateBytes(5000, 6000, 9000),
+                                /* totalOveruses= */ 2)));
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         List<ResourceOveruseStats> expectedStats = Arrays.asList(
-                constructResourceOveruseStats(1103456, "third_party_package",
+                constructResourceOveruseStats(1103456, "third_party_package", 1,
                         packageIoOveruseStats.get(0).ioOveruseStats),
-                constructResourceOveruseStats(1201278, "vendor_package.critical",
+                constructResourceOveruseStats(1201278, "vendor_package.critical", 0,
                         packageIoOveruseStats.get(1).ioOveruseStats));
 
         List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
-                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */0,
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
                 CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
 
         ResourceOveruseStatsSubject.assertThat(actualStats)
                 .containsExactlyElementsIn(expectedStats);
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
+    }
+
+    @Test
+    public void testGetAllResourceOveruseStatsWithNoMinimumForPast7days() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
+        List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
+                constructPackageIoOveruseStats(1103456,
+                        /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
+                constructPackageIoOveruseStats(1201278,
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+                                /* writtenBytes= */ constructPerStateBytes(5000, 6000, 9000),
+                                /* totalOveruses= */ 0)));
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+        ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+        long startTime = now.minusDays(4).toEpochSecond();
+        IoOveruseStats thirdPartyPkgOldStats = new IoOveruseStats.Builder(
+                startTime, now.toEpochSecond() - startTime).setTotalOveruses(5)
+                .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build();
+        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(11, "third_party_package", 6))
+                .thenReturn(thirdPartyPkgOldStats);
+
+        startTime = now.minusDays(6).toEpochSecond();
+        IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
+                startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
+                .setTotalTimesKilled(0).setTotalBytesWritten(35_000).build();
+        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
+                .thenReturn(vendorPkgOldStats);
+
+
+        List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
+                CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+        IoOveruseStats thirdPartyIoStats = new IoOveruseStats.Builder(
+                thirdPartyPkgOldStats.getStartTime(),
+                thirdPartyPkgOldStats.getDurationInSeconds() + STATS_DURATION_SECONDS)
+                .setKillableOnOveruse(true).setTotalOveruses(7).setTotalBytesWritten(24_600)
+                .setTotalTimesKilled(3).setRemainingWriteBytes(new PerStateBytes(0, 0, 0))
+                .build();
+        IoOveruseStats vendorIoStats = new IoOveruseStats.Builder(
+                vendorPkgOldStats.getStartTime(),
+                vendorPkgOldStats.getDurationInSeconds() + STATS_DURATION_SECONDS)
+                .setKillableOnOveruse(false).setTotalOveruses(2).setTotalBytesWritten(55_000)
+                .setTotalTimesKilled(0).setRemainingWriteBytes(new PerStateBytes(450, 120, 340))
+                .build();
+
+        List<ResourceOveruseStats> expectedStats = Arrays.asList(
+                new ResourceOveruseStats.Builder("third_party_package", new UserHandle(11))
+                        .setIoOveruseStats(thirdPartyIoStats).build(),
+                new ResourceOveruseStats.Builder("vendor_package.critical", new UserHandle(12))
+                        .setIoOveruseStats(vendorIoStats).build());
+
+        ResourceOveruseStatsSubject.assertThat(actualStats)
+                .containsExactlyElementsIn(expectedStats);
     }
 
     @Test
@@ -346,46 +551,48 @@
 
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
                 constructPackageIoOveruseStats(1103456,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
                 constructPackageIoOveruseStats(1201000,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
-                                /* writtenBytes= */constructPerStateBytes(5000, 6000, 9000),
-                                /* totalOveruses= */0)),
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+                                /* writtenBytes= */ constructPerStateBytes(5000, 6000, 9000),
+                                /* totalOveruses= */ 0)),
                 constructPackageIoOveruseStats(1303456,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(40, 50, 60),
-                                /* writtenBytes= */constructPerStateBytes(80, 170, 260),
-                                /* totalOveruses= */1)));
+                        /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(80, 170, 260),
+                                /* totalOveruses= */ 1)));
 
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         List<ResourceOveruseStats> expectedStats = Arrays.asList(
-                constructResourceOveruseStats(1103456, "shared:vendor_shared_package",
+                constructResourceOveruseStats(1103456, "shared:vendor_shared_package", 0,
                         packageIoOveruseStats.get(0).ioOveruseStats),
-                constructResourceOveruseStats(1201278, "shared:system_shared_package",
+                constructResourceOveruseStats(1201278, "shared:system_shared_package", 0,
                         packageIoOveruseStats.get(1).ioOveruseStats),
-                constructResourceOveruseStats(1303456, "shared:vendor_shared_package",
+                constructResourceOveruseStats(1303456, "shared:vendor_shared_package", 1,
                         packageIoOveruseStats.get(2).ioOveruseStats));
 
         List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
-                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */0,
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
                 CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
 
         ResourceOveruseStatsSubject.assertThat(actualStats)
                 .containsExactlyElementsIn(expectedStats);
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
     }
 
     @Test
     public void testFailsGetAllResourceOveruseStatsWithInvalidArgs() throws Exception {
         assertThrows(IllegalArgumentException.class,
-                () -> mCarWatchdogService.getAllResourceOveruseStats(0, /* minimumStatsFlag= */0,
+                () -> mCarWatchdogService.getAllResourceOveruseStats(0, /* minimumStatsFlag= */ 0,
                         CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
 
         assertThrows(IllegalArgumentException.class,
@@ -397,13 +604,13 @@
 
         assertThrows(IllegalArgumentException.class,
                 () -> mCarWatchdogService.getAllResourceOveruseStats(
-                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */1 << 5,
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 1 << 5,
                         CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
 
         assertThrows(IllegalArgumentException.class,
                 () -> mCarWatchdogService.getAllResourceOveruseStats(
-                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */0,
-                        /* maxStatsPeriod= */0));
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
+                        /* maxStatsPeriod= */ 0));
     }
 
     @Test
@@ -413,22 +620,20 @@
                 constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
 
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
-                constructPackageIoOveruseStats(1103456,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
-                constructPackageIoOveruseStats(1201278,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
-                                /* writtenBytes= */constructPerStateBytes(7000000, 6000, 9000),
-                                /* totalOveruses= */2)));
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+                constructPackageIoOveruseStats(1103456, /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
+                constructPackageIoOveruseStats(1201278, /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+                                /* writtenBytes= */ constructPerStateBytes(7_000_000, 6000, 9000),
+                                /* totalOveruses= */ 2)));
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         List<ResourceOveruseStats> expectedStats = Collections.singletonList(
-                constructResourceOveruseStats(1201278, "vendor_package.critical",
+                constructResourceOveruseStats(1201278, "vendor_package.critical", 0,
                         packageIoOveruseStats.get(1).ioOveruseStats));
 
         List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
@@ -438,6 +643,64 @@
 
         ResourceOveruseStatsSubject.assertThat(actualStats)
                 .containsExactlyElementsIn(expectedStats);
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
+    }
+
+    @Test
+    public void testGetAllResourceOveruseStatsWithMinimumForPast7days() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
+        List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
+                constructPackageIoOveruseStats(1103456,
+                        /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
+                constructPackageIoOveruseStats(1201278,
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+                                /* writtenBytes= */ constructPerStateBytes(100_000, 6000, 9000),
+                                /* totalOveruses= */ 0)));
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+        ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+        long startTime = now.minusDays(4).toEpochSecond();
+        IoOveruseStats thirdPartyPkgOldStats = new IoOveruseStats.Builder(
+                startTime, now.toEpochSecond() - startTime).setTotalOveruses(5)
+                .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build();
+        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(11, "third_party_package", 6))
+                .thenReturn(thirdPartyPkgOldStats);
+
+        startTime = now.minusDays(6).toEpochSecond();
+        IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
+                startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
+                .setTotalTimesKilled(0).setTotalBytesWritten(6_900_000).build();
+        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
+                .thenReturn(vendorPkgOldStats);
+
+        List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                CarWatchdogManager.FLAG_MINIMUM_STATS_IO_1_MB,
+                CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+        IoOveruseStats vendorIoStats = new IoOveruseStats.Builder(
+                vendorPkgOldStats.getStartTime(),
+                vendorPkgOldStats.getDurationInSeconds() + STATS_DURATION_SECONDS)
+                .setKillableOnOveruse(false).setTotalOveruses(2).setTotalBytesWritten(7_015_000)
+                .setTotalTimesKilled(0).setRemainingWriteBytes(new PerStateBytes(450, 120, 340))
+                .build();
+
+        List<ResourceOveruseStats> expectedStats = Collections.singletonList(
+                new ResourceOveruseStats.Builder("vendor_package.critical", new UserHandle(12))
+                        .setIoOveruseStats(vendorIoStats).build());
+
+        ResourceOveruseStatsSubject.assertThat(actualStats)
+                .containsExactlyElementsIn(expectedStats);
     }
 
     @Test
@@ -448,21 +711,21 @@
 
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
                 constructPackageIoOveruseStats(1103456,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
                 constructPackageIoOveruseStats(1201278,
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
-                                /* writtenBytes= */constructPerStateBytes(500, 600, 900),
-                                /* totalOveruses= */2)));
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+                                /* writtenBytes= */ constructPerStateBytes(500, 600, 900),
+                                /* totalOveruses= */ 2)));
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         ResourceOveruseStats expectedStats =
-                constructResourceOveruseStats(1201278, "vendor_package.critical",
+                constructResourceOveruseStats(1201278, "vendor_package.critical", 0,
                         packageIoOveruseStats.get(1).ioOveruseStats);
 
         ResourceOveruseStats actualStats =
@@ -471,10 +734,58 @@
                         CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
                         CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
 
-        assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
-                + actualStats.toString())
-                .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
     }
+
+    @Test
+    public void testGetResourceOveruseStatsForUserPackageForPast7days() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
+        List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
+                constructPackageIoOveruseStats(1103456,
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
+                constructPackageIoOveruseStats(1201278,
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+                                /* writtenBytes= */ constructPerStateBytes(500, 600, 900),
+                                /* totalOveruses= */ 2)));
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+        ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+        long startTime = now.minusDays(4).toEpochSecond();
+        IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
+                startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
+                .setTotalTimesKilled(0).setTotalBytesWritten(6_900_000).build();
+        when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
+                .thenReturn(vendorPkgOldStats);
+
+        ResourceOveruseStats actualStats =
+                mCarWatchdogService.getResourceOveruseStatsForUserPackage(
+                        "vendor_package.critical", new UserHandle(12),
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                        CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+        IoOveruseStats vendorIoStats = new IoOveruseStats.Builder(
+                vendorPkgOldStats.getStartTime(),
+                vendorPkgOldStats.getDurationInSeconds() + STATS_DURATION_SECONDS)
+                .setKillableOnOveruse(false).setTotalOveruses(4).setTotalBytesWritten(6_902_000)
+                .setTotalTimesKilled(0).setRemainingWriteBytes(new PerStateBytes(450, 120, 340))
+                .build();
+
+        ResourceOveruseStats expectedStats = new ResourceOveruseStats.Builder(
+                "vendor_package.critical", new UserHandle(12)).setIoOveruseStats(vendorIoStats)
+                .build();
+
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+    }
+
     @Test
     public void testGetResourceOveruseStatsForUserPackageWithSharedUids() throws Exception {
         injectPackageInfos(Arrays.asList(
@@ -487,12 +798,12 @@
 
         SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
                 injectIoOveruseStatsForPackages(
-                        mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(
+                        mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(
                                 Collections.singleton("shared:vendor_shared_package")),
-                        /* shouldNotifyPackages= */new ArraySet<>());
+                        /* shouldNotifyPackages= */ new ArraySet<>());
 
         ResourceOveruseStats expectedStats =
-                constructResourceOveruseStats(1103456, "shared:vendor_shared_package",
+                constructResourceOveruseStats(1103456, "shared:vendor_shared_package", 0,
                         packageIoOveruseStatsByUid.get(1103456).ioOveruseStats);
 
         ResourceOveruseStats actualStats =
@@ -501,22 +812,20 @@
                         CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
                         CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
 
-        assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
-                + actualStats.toString())
-                .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
     }
 
     @Test
     public void testFailsGetResourceOveruseStatsForUserPackageWithInvalidArgs() throws Exception {
         assertThrows(NullPointerException.class,
                 () -> mCarWatchdogService.getResourceOveruseStatsForUserPackage(
-                        /* packageName= */null, new UserHandle(10),
+                        /* packageName= */ null, new UserHandle(10),
                         CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
                         CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
 
         assertThrows(NullPointerException.class,
                 () -> mCarWatchdogService.getResourceOveruseStatsForUserPackage("some.package",
-                        /* userHandle= */null, CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                        /* userHandle= */ null, CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
                         CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
 
         assertThrows(IllegalArgumentException.class,
@@ -526,13 +835,13 @@
 
         assertThrows(IllegalArgumentException.class,
                 () -> mCarWatchdogService.getResourceOveruseStatsForUserPackage("some.package",
-                        new UserHandle(10), /* resourceOveruseFlag= */0,
+                        new UserHandle(10), /* resourceOveruseFlag= */ 0,
                         CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
 
         assertThrows(IllegalArgumentException.class,
                 () -> mCarWatchdogService.getResourceOveruseStatsForUserPackage("some.package",
                         new UserHandle(10), CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
-                        /* maxStatsPeriod= */0));
+                        /* maxStatsPeriod= */ 0));
     }
 
     @Test
@@ -556,8 +865,8 @@
         verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         injectIoOveruseStatsForPackages(
-                mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singleton(mMockContext.getPackageName())));
 
         verify(mockListener).onOveruse(any());
@@ -568,8 +877,8 @@
         verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         injectIoOveruseStatsForPackages(
-                mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singletonList(mMockContext.getPackageName())));
 
         verifyNoMoreInteractions(mockListener);
@@ -617,8 +926,8 @@
         verify(secondMockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         injectIoOveruseStatsForPackages(
-                mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singleton(mMockContext.getPackageName())));
 
         verify(firstMockListener).onOveruse(any());
@@ -629,8 +938,8 @@
         verify(firstMockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         injectIoOveruseStatsForPackages(
-                mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singletonList(mMockContext.getPackageName())));
 
         verify(secondMockListener, times(2)).onOveruse(any());
@@ -641,8 +950,8 @@
         verify(secondMockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         injectIoOveruseStatsForPackages(
-                mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singletonList(mMockContext.getPackageName())));
 
         verifyNoMoreInteractions(firstMockListener);
@@ -670,13 +979,13 @@
         verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
         List<PackageIoOveruseStats> packageIoOveruseStats = Collections.singletonList(
-                constructPackageIoOveruseStats(callingUid, /* shouldNotify= */true,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)));
+                constructPackageIoOveruseStats(callingUid, /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)));
 
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         verify(mockListener).onOveruse(any());
 
@@ -685,7 +994,7 @@
         verify(mockListener, atLeastOnce()).asBinder();
         verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         verifyNoMoreInteractions(mockListener);
     }
@@ -901,38 +1210,34 @@
 
     @Test
     public void testGetPackageKillableStatesAsUserWithSafeToKillPackages() throws Exception {
-        android.automotive.watchdog.internal.ResourceOveruseConfiguration systemConfig =
-                new android.automotive.watchdog.internal.ResourceOveruseConfiguration();
-        systemConfig.componentType = ComponentType.SYSTEM;
-        systemConfig.safeToKillPackages = Collections.singletonList("system_package.non_critical");
-        android.automotive.watchdog.internal.ResourceOveruseConfiguration vendorConfig =
-                new android.automotive.watchdog.internal.ResourceOveruseConfiguration();
-        vendorConfig.componentType = ComponentType.VENDOR;
-        vendorConfig.safeToKillPackages = Collections.singletonList("vendor_package.non_critical");
-        when(mMockCarWatchdogDaemon.getResourceOveruseConfigurations())
-                .thenReturn(Arrays.asList(systemConfig, vendorConfig));
-        mCarWatchdogService.init();
         mockUmGetAliveUsers(mMockUserManager, 11, 12);
+        List<android.automotive.watchdog.internal.ResourceOveruseConfiguration> configs =
+                sampleInternalResourceOveruseConfigurations();
+        injectResourceOveruseConfigsAndWait(configs);
 
         injectPackageInfos(Arrays.asList(
-                constructPackageManagerPackageInfo("system_package.non_critical", 1102459, null),
+                constructPackageManagerPackageInfo("system_package.non_critical.A", 1102459, null),
                 constructPackageManagerPackageInfo("third_party_package", 1103456, null),
-                constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
-                constructPackageManagerPackageInfo("vendor_package.non_critical", 1105573, null),
+                constructPackageManagerPackageInfo("vendor_package.critical.B", 1101278, null),
+                constructPackageManagerPackageInfo("vendor_package.non_critical.A", 1105573, null),
                 constructPackageManagerPackageInfo("third_party_package", 1203456, null),
-                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+                constructPackageManagerPackageInfo("vendor_package.critical.B", 1201278, null)));
 
         PackageKillableStateSubject.assertThat(
-                mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(11)))
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL))
                 .containsExactly(
-                        new PackageKillableState("system_package.non_critical", 11,
+                        new PackageKillableState("system_package.non_critical.A", 11,
                                 PackageKillableState.KILLABLE_STATE_YES),
                         new PackageKillableState("third_party_package", 11,
                                 PackageKillableState.KILLABLE_STATE_YES),
-                        new PackageKillableState("vendor_package.critical", 11,
+                        new PackageKillableState("vendor_package.critical.B", 11,
                                 PackageKillableState.KILLABLE_STATE_NEVER),
-                        new PackageKillableState("vendor_package.non_critical", 11,
-                                PackageKillableState.KILLABLE_STATE_YES));
+                        new PackageKillableState("vendor_package.non_critical.A", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("third_party_package", 12,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("vendor_package.critical.B", 12,
+                                PackageKillableState.KILLABLE_STATE_NEVER));
     }
 
     @Test
@@ -968,15 +1273,13 @@
     @Test
     public void testGetPackageKillableStatesAsUserWithSharedUidsAndSafeToKillPackages()
             throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 11);
         android.automotive.watchdog.internal.ResourceOveruseConfiguration vendorConfig =
                 new android.automotive.watchdog.internal.ResourceOveruseConfiguration();
         vendorConfig.componentType = ComponentType.VENDOR;
         vendorConfig.safeToKillPackages = Collections.singletonList(
                 "vendor_package.non_critical.A");
-        when(mMockCarWatchdogDaemon.getResourceOveruseConfigurations())
-                .thenReturn(Arrays.asList(vendorConfig));
-        mCarWatchdogService.init();
-        mockUmGetAliveUsers(mMockUserManager, 11);
+        injectResourceOveruseConfigsAndWait(Collections.singletonList(vendorConfig));
 
         injectPackageInfos(Arrays.asList(
                 constructPackageManagerPackageInfo(
@@ -1008,15 +1311,13 @@
     @Test
     public void testGetPackageKillableStatesAsUserWithSharedUidsAndSafeToKillSharedPackage()
             throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 11);
         android.automotive.watchdog.internal.ResourceOveruseConfiguration vendorConfig =
                 new android.automotive.watchdog.internal.ResourceOveruseConfiguration();
         vendorConfig.componentType = ComponentType.VENDOR;
         vendorConfig.safeToKillPackages = Collections.singletonList(
                 "shared:vendor_shared_package.B");
-        when(mMockCarWatchdogDaemon.getResourceOveruseConfigurations())
-                .thenReturn(Arrays.asList(vendorConfig));
-        mCarWatchdogService.init();
-        mockUmGetAliveUsers(mMockUserManager, 11);
+        injectResourceOveruseConfigsAndWait(Collections.singletonList(vendorConfig));
 
         injectPackageInfos(Arrays.asList(
                 constructPackageManagerPackageInfo(
@@ -1304,7 +1605,7 @@
                         CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
 
         /* Method initially called in CarWatchdogService init */
-        verify(mMockCarWatchdogDaemon, times(1)).getResourceOveruseConfigurations();
+        verify(mMockCarWatchdogDaemon).getResourceOveruseConfigurations();
     }
 
     @Test
@@ -1379,11 +1680,10 @@
     @Test
     public void testLatestIoOveruseStats() throws Exception {
         int criticalSysPkgUid = Binder.getCallingUid();
-        int nonCriticalSysPkgUid = getUid(1056);
-        int nonCriticalVndrPkgUid = getUid(2564);
-        int thirdPartyPkgUid = getUid(2044);
+        int nonCriticalSysPkgUid = 1001056;
+        int nonCriticalVndrPkgUid = 1002564;
+        int thirdPartyPkgUid = 1002044;
 
-        mCarWatchdogService.setResourceOveruseKillingDelay(1000);
         injectPackageInfos(Arrays.asList(
                 constructPackageManagerPackageInfo(
                         "system_package.critical", criticalSysPkgUid, null),
@@ -1402,68 +1702,58 @@
         mCarWatchdogService.addResourceOveruseListener(
                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, mockListener);
 
-        IPackageManager packageManagerService = Mockito.spy(ActivityThread.getPackageManager());
-        when(ActivityThread.getPackageManager()).thenReturn(packageManagerService);
-        mockApplicationEnabledSettingAccessors(packageManagerService);
-
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
                 /* Overuse occurred but cannot be killed/disabled. */
-                constructPackageIoOveruseStats(criticalSysPkgUid, /* shouldNotify= */true,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(0, 0, 0),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
+                constructPackageIoOveruseStats(criticalSysPkgUid, /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
                 /* No overuse occurred but should be notified. */
-                constructPackageIoOveruseStats(nonCriticalSysPkgUid, /* shouldNotify= */true,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 30, 40),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
+                constructPackageIoOveruseStats(nonCriticalSysPkgUid, /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(20, 30, 40),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
                 /* Neither overuse occurred nor be notified. */
-                constructPackageIoOveruseStats(nonCriticalVndrPkgUid, /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(200, 300, 400),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
+                constructPackageIoOveruseStats(nonCriticalVndrPkgUid, /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(200, 300, 400),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
                 /* Overuse occurred and can be killed/disabled. */
-                constructPackageIoOveruseStats(thirdPartyPkgUid, /* shouldNotify= */true,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(0, 0, 0),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)));
+                constructPackageIoOveruseStats(thirdPartyPkgUid, /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)));
 
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
-        /*
-         * Handling of packages that exceed I/O thresholds is done on the main thread. To ensure
-         * the handling completes before verification, wait for the message to be posted on the
-         * main thread and execute an empty block on the main thread.
-         */
-        delayedRunOnMainSync(() -> {}, /* delayMillis= */2000);
+        assertThat(mDisabledUserPackages).containsExactlyElementsIn(Collections.singleton(
+                "10:third_party_package"));
 
         List<ResourceOveruseStats> expectedStats = new ArrayList<>();
 
-        expectedStats.add(constructResourceOveruseStats(criticalSysPkgUid,
-                "system_package.critical", packageIoOveruseStats.get(0).ioOveruseStats));
+        expectedStats.add(constructResourceOveruseStats(
+                criticalSysPkgUid, "system_package.critical", 0,
+                packageIoOveruseStats.get(0).ioOveruseStats));
 
         verifyOnOveruseCalled(expectedStats, mockListener);
 
-        expectedStats.add(constructResourceOveruseStats(nonCriticalSysPkgUid,
-                "system_package.non_critical", packageIoOveruseStats.get(1).ioOveruseStats));
+        expectedStats.add(constructResourceOveruseStats(
+                nonCriticalSysPkgUid, "system_package.non_critical", 0,
+                packageIoOveruseStats.get(1).ioOveruseStats));
 
-        expectedStats.add(constructResourceOveruseStats(thirdPartyPkgUid,
-                "third_party_package",
+        /*
+         * When the package receives overuse notification, the package is not yet killed so the
+         * totalTimesKilled counter is not yet incremented.
+         */
+        expectedStats.add(constructResourceOveruseStats(thirdPartyPkgUid, "third_party_package", 0,
                 packageIoOveruseStats.get(3).ioOveruseStats));
 
         verifyOnOveruseCalled(expectedStats, mockSystemListener);
 
-        verify(packageManagerService).getApplicationEnabledSetting(
-                eq("third_party_package"),
-                eq(UserHandle.getUserId(thirdPartyPkgUid)));
-        verify(packageManagerService).setApplicationEnabledSetting(
-                eq("third_party_package"), anyInt(), anyInt(),
-                eq(UserHandle.getUserId(thirdPartyPkgUid)), anyString());
-
         List<PackageResourceOveruseAction> expectedActions = Arrays.asList(
                 constructPackageResourceOveruseAction(
                         "system_package.critical",
@@ -1477,10 +1767,9 @@
     @Test
     public void testLatestIoOveruseStatsWithSharedUid() throws Exception {
         int criticalSysSharedUid = Binder.getCallingUid();
-        int nonCriticalVndrSharedUid = getUid(2564);
-        int thirdPartySharedUid = getUid(2044);
+        int nonCriticalVndrSharedUid = 1002564;
+        int thirdPartySharedUid = 1002044;
 
-        mCarWatchdogService.setResourceOveruseKillingDelay(1000);
         injectPackageInfos(Arrays.asList(
                 constructPackageManagerPackageInfo(
                         "system_package.A", criticalSysSharedUid, "system_shared_package"),
@@ -1502,68 +1791,53 @@
         mCarWatchdogService.addResourceOveruseListener(
                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, mockListener);
 
-        IPackageManager packageManagerService = Mockito.spy(ActivityThread.getPackageManager());
-        when(ActivityThread.getPackageManager()).thenReturn(packageManagerService);
-        mockApplicationEnabledSettingAccessors(packageManagerService);
-
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
                 /* Overuse occurred but cannot be killed/disabled. */
-                constructPackageIoOveruseStats(criticalSysSharedUid, /* shouldNotify= */true,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(0, 0, 0),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
+                constructPackageIoOveruseStats(criticalSysSharedUid, /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
                 /* No overuse occurred but should be notified. */
-                constructPackageIoOveruseStats(nonCriticalVndrSharedUid, /* shouldNotify= */true,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(200, 300, 400),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
+                constructPackageIoOveruseStats(nonCriticalVndrSharedUid, /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(200, 300, 400),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)),
                 /* Overuse occurred and can be killed/disabled. */
-                constructPackageIoOveruseStats(thirdPartySharedUid, /* shouldNotify= */true,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(0, 0, 0),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)));
+                constructPackageIoOveruseStats(thirdPartySharedUid, /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                                /* totalOveruses= */ 2)));
 
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
-        /*
-         * Handling of packages that exceed I/O thresholds is done on the main thread. To ensure
-         * the handling completes before verification, wait for the message to be posted on the
-         * main thread and execute an empty block on the main thread.
-         */
-        delayedRunOnMainSync(() -> {}, /* delayMillis= */2000);
+        assertThat(mDisabledUserPackages).containsExactlyElementsIn(Arrays.asList(
+                "10:third_party_package.A", "10:third_party_package.B"));
 
         List<ResourceOveruseStats> expectedStats = new ArrayList<>();
 
-        expectedStats.add(constructResourceOveruseStats(criticalSysSharedUid,
-                "shared:system_shared_package", packageIoOveruseStats.get(0).ioOveruseStats));
+        expectedStats.add(constructResourceOveruseStats(
+                criticalSysSharedUid, "shared:system_shared_package", 0,
+                packageIoOveruseStats.get(0).ioOveruseStats));
 
         verifyOnOveruseCalled(expectedStats, mockListener);
 
-        expectedStats.add(constructResourceOveruseStats(nonCriticalVndrSharedUid,
-                "shared:vendor_shared_package", packageIoOveruseStats.get(1).ioOveruseStats));
+        expectedStats.add(constructResourceOveruseStats(
+                nonCriticalVndrSharedUid, "shared:vendor_shared_package", 0,
+                packageIoOveruseStats.get(1).ioOveruseStats));
 
-        expectedStats.add(constructResourceOveruseStats(thirdPartySharedUid,
-                "shared:third_party_shared_package",
+        /*
+         * When the package receives overuse notification, the package is not yet killed so the
+         * totalTimesKilled counter is not yet incremented.
+         */
+        expectedStats.add(constructResourceOveruseStats(
+                thirdPartySharedUid, "shared:third_party_shared_package", 0,
                 packageIoOveruseStats.get(2).ioOveruseStats));
 
         verifyOnOveruseCalled(expectedStats, mockSystemListener);
 
-        verify(packageManagerService).getApplicationEnabledSetting(
-                eq("third_party_package.A"),
-                eq(UserHandle.getUserId(thirdPartySharedUid)));
-        verify(packageManagerService).getApplicationEnabledSetting(
-                eq("third_party_package.B"),
-                eq(UserHandle.getUserId(thirdPartySharedUid)));
-        verify(packageManagerService).setApplicationEnabledSetting(
-                eq("third_party_package.A"), anyInt(), anyInt(),
-                eq(UserHandle.getUserId(thirdPartySharedUid)), anyString());
-        verify(packageManagerService).setApplicationEnabledSetting(
-                eq("third_party_package.B"), anyInt(), anyInt(),
-                eq(UserHandle.getUserId(thirdPartySharedUid)), anyString());
-
         List<PackageResourceOveruseAction> expectedActions = Arrays.asList(
                 constructPackageResourceOveruseAction(
                         "shared:system_shared_package",
@@ -1588,12 +1862,141 @@
     }
 
     @Test
+    public void testPersistStatsOnShutdownEnter() throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 10, 11, 12);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "third_party_package", 1103456, "vendor_shared_package.critical"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package", 1103456, "vendor_shared_package.critical"),
+                constructPackageManagerPackageInfo("third_party_package.A", 1001100, null),
+                constructPackageManagerPackageInfo("third_party_package.A", 1201100, null)));
+
+        SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+                injectIoOveruseStatsForPackages(
+                        mGenericPackageNameByUid,
+                        /* killablePackages= */ new ArraySet<>(Collections.singletonList(
+                                "third_party_package.A")),
+                        /* shouldNotifyPackages= */ new ArraySet<>());
+
+        mCarWatchdogService.setKillablePackageAsUser(
+                "third_party_package.A", new UserHandle(12), /* isKillable= */ false);
+
+        mCarPowerStateListener.onStateChanged(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);
+
+        List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
+                CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+        List<ResourceOveruseStats> expectedStats = Arrays.asList(
+                constructResourceOveruseStats(
+                        /* uid= */ 1103456, "shared:vendor_shared_package.critical",
+                        /* totalTimesKilled= */ 0,
+                        packageIoOveruseStatsByUid.get(1103456).ioOveruseStats),
+                constructResourceOveruseStats(
+                        /* uid= */ 1001100, "third_party_package.A", /* totalTimesKilled= */ 0,
+                        packageIoOveruseStatsByUid.get(1001100).ioOveruseStats),
+                constructResourceOveruseStats(
+                        /* uid= */ 1201100, "third_party_package.A", /* totalTimesKilled= */ 0,
+                        packageIoOveruseStatsByUid.get(1201100).ioOveruseStats));
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL))
+                .containsExactly(
+                        new PackageKillableState("third_party_package", 11,
+                                PackageKillableState.KILLABLE_STATE_NEVER),
+                        new PackageKillableState("vendor_package", 11,
+                                PackageKillableState.KILLABLE_STATE_NEVER),
+                        new PackageKillableState("third_party_package.A", 10,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("third_party_package.A", 12,
+                                PackageKillableState.KILLABLE_STATE_NO));
+
+        ResourceOveruseStatsSubject.assertThat(actualStats)
+                .containsExactlyElementsIn(expectedStats);
+
+        verifyNoMoreInteractions(mMockWatchdogStorage);
+    }
+
+    @Test
+    public void testPersistIoOveruseStatsOnDateChange() throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 10);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("system_package", 1011200, null),
+                constructPackageManagerPackageInfo("third_party_package", 1001100, null)));
+
+        setDate(1);
+        List<PackageIoOveruseStats> prevDayStats = Arrays.asList(
+                constructPackageIoOveruseStats(1011200, /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(600, 700, 800),
+                                /* totalOveruses= */ 2)),
+                constructPackageIoOveruseStats(1001100, /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(50, 60, 70),
+                                /* writtenBytes= */ constructPerStateBytes(1100, 1200, 1300),
+                                /* totalOveruses= */ 5)));
+        pushLatestIoOveruseStatsAndWait(prevDayStats);
+
+        List<WatchdogStorage.IoUsageStatsEntry> expectedSavedEntries = Arrays.asList(
+                new WatchdogStorage.IoUsageStatsEntry(/* userId= */ 10, "system_package",
+                new WatchdogPerfHandler.PackageIoUsage(prevDayStats.get(0).ioOveruseStats,
+                        /* forgivenWriteBytes= */ constructPerStateBytes(600, 700, 800),
+                        /* totalTimesKilled= */ 1)),
+                new WatchdogStorage.IoUsageStatsEntry(/* userId= */ 10, "third_party_package",
+                        new WatchdogPerfHandler.PackageIoUsage(prevDayStats.get(1).ioOveruseStats,
+                                /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* totalTimesKilled= */ 0)));
+
+        setDate(0);
+        List<PackageIoOveruseStats> currentDayStats = Arrays.asList(
+                constructPackageIoOveruseStats(1011200, /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(500, 550, 600),
+                                /* writtenBytes= */ constructPerStateBytes(100, 150, 200),
+                                /* totalOveruses= */ 0)),
+                constructPackageIoOveruseStats(1001100, /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(250, 360, 470),
+                                /* writtenBytes= */ constructPerStateBytes(900, 900, 900),
+                                /* totalOveruses= */ 0)));
+        pushLatestIoOveruseStatsAndWait(currentDayStats);
+
+        IoUsageStatsEntrySubject.assertThat(mIoUsageStatsEntries)
+                .containsExactlyElementsIn(expectedSavedEntries);
+
+        List<ResourceOveruseStats> actualCurrentDayStats =
+                mCarWatchdogService.getAllResourceOveruseStats(
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
+                        CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+        List<ResourceOveruseStats> expectedCurrentDayStats = Arrays.asList(
+                constructResourceOveruseStats(
+                        /* uid= */ 1011200, "system_package", /* totalTimesKilled= */ 0,
+                        currentDayStats.get(0).ioOveruseStats),
+                constructResourceOveruseStats(
+                        /* uid= */ 1001100, "third_party_package", /* totalTimesKilled= */ 0,
+                        currentDayStats.get(1).ioOveruseStats));
+
+        ResourceOveruseStatsSubject.assertThat(actualCurrentDayStats)
+                .containsExactlyElementsIn(expectedCurrentDayStats);
+    }
+
+    @Test
     public void testResetResourceOveruseStats() throws Exception {
         mGenericPackageNameByUid.put(Binder.getCallingUid(), mMockContext.getPackageName());
         mGenericPackageNameByUid.put(1101278, "vendor_package.critical");
         injectIoOveruseStatsForPackages(
-                mGenericPackageNameByUid, /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>());
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>());
 
         mWatchdogServiceForSystemImpl.resetResourceOveruseStats(
                 Collections.singletonList(mMockContext.getPackageName()));
@@ -1606,9 +2009,7 @@
                 mMockContext.getPackageName(),
                 UserHandle.getUserHandleForUid(Binder.getCallingUid())).build();
 
-        assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
-                + actualStats.toString())
-                .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
     }
 
     @Test
@@ -1787,16 +2188,105 @@
         verify(mMockCarWatchdogDaemon, never()).controlProcessHealthCheck(anyBoolean());
     }
 
+    public static android.automotive.watchdog.PerStateBytes constructPerStateBytes(
+            long fgBytes, long bgBytes, long gmBytes) {
+        android.automotive.watchdog.PerStateBytes perStateBytes =
+                new android.automotive.watchdog.PerStateBytes();
+        perStateBytes.foregroundBytes = fgBytes;
+        perStateBytes.backgroundBytes = bgBytes;
+        perStateBytes.garageModeBytes = gmBytes;
+        return perStateBytes;
+    }
+
     private void mockWatchdogDaemon() {
         when(mMockBinder.queryLocalInterface(anyString())).thenReturn(mMockCarWatchdogDaemon);
         when(mMockCarWatchdogDaemon.asBinder()).thenReturn(mMockBinder);
         doReturn(mMockBinder).when(() -> ServiceManager.getService(CAR_WATCHDOG_DAEMON_INTERFACE));
+        mIsDaemonCrashed = false;
+    }
+
+    private void mockWatchdogStorage() {
+        when(mMockWatchdogStorage.saveUserPackageSettings(any())).thenAnswer((args) -> {
+            mUserPackageSettingsEntries.addAll(args.getArgument(0));
+            return true;
+        });
+        when(mMockWatchdogStorage.saveIoUsageStats(any())).thenAnswer((args) -> {
+            List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = args.getArgument(0);
+            for (WatchdogStorage.IoUsageStatsEntry entry : ioUsageStatsEntries) {
+                mIoUsageStatsEntries.add(
+                        new WatchdogStorage.IoUsageStatsEntry(entry.userId, entry.packageName,
+                                new WatchdogPerfHandler.PackageIoUsage(
+                                        entry.ioUsage.getInternalIoOveruseStats(),
+                                        entry.ioUsage.getForgivenWriteBytes(),
+                                        entry.ioUsage.getTotalTimesKilled())));
+            }
+            return true;
+        });
+        when(mMockWatchdogStorage.getUserPackageSettings()).thenReturn(mUserPackageSettingsEntries);
+        when(mMockWatchdogStorage.getTodayIoUsageStats()).thenReturn(mIoUsageStatsEntries);
+    }
+
+    private void setupUsers() {
+        when(mMockContext.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager);
+        mockUmGetAllUsers(mMockUserManager, new UserInfo[0]);
+    }
+
+    private void captureCarPowerStateListener() {
+        ArgumentCaptor<ICarPowerStateListener> receiverArgumentCaptor =
+                ArgumentCaptor.forClass(ICarPowerStateListener.class);
+        verify(mMockCarPowerManagementService).registerListener(receiverArgumentCaptor.capture());
+        mCarPowerStateListener = receiverArgumentCaptor.getValue();
+        assertWithMessage("Car power state listener must be non-null").that(mCarPowerStateListener)
+                .isNotNull();
+    }
+
+    private void captureBroadcastReceiver() {
+        ArgumentCaptor<BroadcastReceiver> receiverArgumentCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mMockContext)
+                .registerReceiverForAllUsers(receiverArgumentCaptor.capture(), any(), any(), any());
+        mBroadcastReceiver = receiverArgumentCaptor.getValue();
+        assertWithMessage("Broadcast receiver must be non-null").that(mBroadcastReceiver)
+                .isNotNull();
+    }
+
+    private void captureWatchdogServiceForSystem() throws Exception {
+        /* Registering to daemon is done on the main thread. To ensure the registration completes
+         * before verification, execute an empty block on the main thread.
+         */
+        CarServiceUtils.runOnMainSync(() -> {});
+
+        ArgumentCaptor<ICarWatchdogServiceForSystem> watchdogServiceForSystemImplCaptor =
+                ArgumentCaptor.forClass(ICarWatchdogServiceForSystem.class);
+        verify(mMockCarWatchdogDaemon, atLeastOnce()).registerCarWatchdogService(
+                watchdogServiceForSystemImplCaptor.capture());
+        mWatchdogServiceForSystemImpl = watchdogServiceForSystemImplCaptor.getValue();
+        assertWithMessage("Car watchdog service for system must be non-null")
+                .that(mCarPowerStateListener).isNotNull();
+    }
+
+    private void captureDaemonBinderDeathRecipient() throws Exception {
+        ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor =
+                ArgumentCaptor.forClass(IBinder.DeathRecipient.class);
+        verify(mMockBinder, timeout(MAX_WAIT_TIME_MS).atLeastOnce())
+                .linkToDeath(deathRecipientCaptor.capture(), anyInt());
+        mCarWatchdogDaemonBinderDeathRecipient = deathRecipientCaptor.getValue();
+        assertWithMessage("Binder death recipient must be non-null").that(mBroadcastReceiver)
+                .isNotNull();
+    }
+
+    private void verifyDatabaseInit(int wantedInvocations) throws Exception {
+        /*
+         * Database read is posted on a separate handler thread. Wait until the handler thread has
+         * processed the database read request before verifying.
+         */
+        CarServiceUtils.getHandlerThread(CarWatchdogService.class.getSimpleName())
+                .getThreadHandler().post(() -> {});
+        verify(mMockWatchdogStorage, times(wantedInvocations)).getUserPackageSettings();
+        verify(mMockWatchdogStorage, times(wantedInvocations)).getTodayIoUsageStats();
     }
 
     private void mockPackageManager() throws Exception {
-        mGenericPackageNameByUid.clear();
-        mPackagesBySharedUid.clear();
-        mPmPackageInfoByUserPackage.clear();
         when(mMockPackageManager.getNamesForUids(any())).thenAnswer(args -> {
             int[] uids = args.getArgument(0);
             String[] names = new String[uids.length];
@@ -1846,32 +2336,26 @@
                     }
                     return packageInfos;
                 });
+        IPackageManager pm = Mockito.spy(ActivityThread.getPackageManager());
+        when(ActivityThread.getPackageManager()).thenReturn(pm);
+        doAnswer((args) -> {
+            String value = args.getArgument(3) + ":" + args.getArgument(0);
+            mDisabledUserPackages.add(value);
+            return null;
+        }).when(pm).setApplicationEnabledSetting(
+                anyString(), eq(COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED), anyInt(),
+                anyInt(), anyString());
+        doReturn(COMPONENT_ENABLED_STATE_ENABLED).when(pm)
+                .getApplicationEnabledSetting(anyString(), anyInt());
     }
 
-    private void captureBroadcastReceiver() {
-        ArgumentCaptor<BroadcastReceiver> receiverArgumentCaptor =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mMockContext)
-                .registerReceiverForAllUsers(receiverArgumentCaptor.capture(), any(), any(), any());
-        mBroadcastReceiver = receiverArgumentCaptor.getValue();
-        assertWithMessage("Broadcast receiver must be non-null").that(mBroadcastReceiver)
-                .isNotEqualTo(null);
-    }
-
-    private void captureDaemonBinderDeathRecipient() throws Exception {
-        ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor =
-                ArgumentCaptor.forClass(IBinder.DeathRecipient.class);
-        verify(mMockBinder, timeout(MAX_WAIT_TIME_MS).atLeastOnce())
-                .linkToDeath(deathRecipientCaptor.capture(), anyInt());
-        mCarWatchdogDaemonBinderDeathRecipient = deathRecipientCaptor.getValue();
-    }
-
-    public void crashWatchdogDaemon() {
+    private void crashWatchdogDaemon() {
         doReturn(null).when(() -> ServiceManager.getService(CAR_WATCHDOG_DAEMON_INTERFACE));
         mCarWatchdogDaemonBinderDeathRecipient.binderDied();
+        mIsDaemonCrashed = true;
     }
 
-    public void restartWatchdogDaemonAndAwait() throws Exception {
+    private void restartWatchdogDaemonAndAwait() throws Exception {
         CountDownLatch latch = new CountDownLatch(1);
         doAnswer(args -> {
             latch.countDown();
@@ -1879,14 +2363,33 @@
         }).when(mMockBinder).linkToDeath(any(), anyInt());
         mockWatchdogDaemon();
         latch.await(MAX_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+        /* On daemon connect, CarWatchdogService posts a new message on the main thread to fetch
+         * the resource overuse configs. Post a message on the same thread and wait until the fetch
+         * completes, so the tests are deterministic.
+         */
+        CarServiceUtils.runOnMainSync(() -> {});
     }
 
-    private void setupUsers() {
-        when(mMockContext.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager);
-        mockUmGetAllUsers(mMockUserManager, new UserInfo[0]);
+    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 verifyResourceOveruseConfigurationsSynced(int wantedNumberOfInvocations)
+    private void verifyResourceOveruseConfigurationsSynced(int wantedInvocations)
             throws Exception {
         /*
          * Syncing the resource configuration in the service with the daemon is done on the main
@@ -1894,22 +2397,9 @@
          * main thread.
          */
         CarServiceUtils.runOnMainSync(() -> {});
-        verify(mMockCarWatchdogDaemon,
-                times(wantedNumberOfInvocations)).getResourceOveruseConfigurations();
+        verify(mMockCarWatchdogDaemon, times(wantedInvocations)).getResourceOveruseConfigurations();
     }
 
-    private ICarWatchdogServiceForSystem registerCarWatchdogService() throws Exception {
-        /* Registering to daemon is done on the main thread. To ensure the registration completes
-         * before verification, execute an empty block on the main thread.
-         */
-        CarServiceUtils.runOnMainSync(() -> {});
-
-        ArgumentCaptor<ICarWatchdogServiceForSystem> watchdogServiceForSystemImplCaptor =
-                ArgumentCaptor.forClass(ICarWatchdogServiceForSystem.class);
-        verify(mMockCarWatchdogDaemon, atLeastOnce()).registerCarWatchdogService(
-                watchdogServiceForSystemImplCaptor.capture());
-        return watchdogServiceForSystemImplCaptor.getValue();
-    }
 
     private void testClientHealthCheck(TestClient client, int badClientCount) throws Exception {
         mCarWatchdogService.registerClient(client, TIMEOUT_CRITICAL);
@@ -1933,6 +2423,17 @@
         return resourceOveruseConfigurationsCaptor.getValue();
     }
 
+    private void injectResourceOveruseConfigsAndWait(
+            List<android.automotive.watchdog.internal.ResourceOveruseConfiguration> configs)
+            throws Exception {
+        when(mMockCarWatchdogDaemon.getResourceOveruseConfigurations()).thenReturn(configs);
+        /* Trigger CarWatchdogService to fetch/sync resource overuse configurations by changing the
+         * daemon connection status from connected -> disconnected -> connected.
+         */
+        crashWatchdogDaemon();
+        restartWatchdogDaemonAndAwait();
+    }
+
     private SparseArray<PackageIoOveruseStats> injectIoOveruseStatsForPackages(
             SparseArray<String> genericPackageNameByUid, Set<String> killablePackages,
             Set<String> shouldNotifyPackages) throws Exception {
@@ -1944,13 +2445,13 @@
             PackageIoOveruseStats stats = constructPackageIoOveruseStats(uid,
                     shouldNotifyPackages.contains(name),
                     constructInternalIoOveruseStats(killablePackages.contains(name),
-                            /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                            /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                            /* totalOveruses= */2));
+                            /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+                            /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+                            /* totalOveruses= */ 2));
             packageIoOveruseStatsByUid.put(uid, stats);
             packageIoOveruseStats.add(stats);
         }
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
         return packageIoOveruseStatsByUid;
     }
 
@@ -1971,23 +2472,26 @@
                 mPackagesBySharedUid.put(uid, packages);
             }
             String userPackageId = userId + ":" + packageInfo.packageName;
-            assertWithMessage("Duplicate package infos provided for user package id: "
-                    + userPackageId).that(mPmPackageInfoByUserPackage.containsKey(userPackageId))
+            assertWithMessage("Duplicate package infos provided for user package id: %s",
+                    userPackageId).that(mPmPackageInfoByUserPackage.containsKey(userPackageId))
                     .isFalse();
-            assertWithMessage("Mismatch generic package names for the same uid '"
-                    + uid + "'").that(mGenericPackageNameByUid.get(uid, genericPackageName))
+            assertWithMessage("Mismatch generic package names for the same uid '%s'",
+                    uid).that(mGenericPackageNameByUid.get(uid, genericPackageName))
                     .isEqualTo(genericPackageName);
             mPmPackageInfoByUserPackage.put(userPackageId, packageInfo);
             mGenericPackageNameByUid.put(uid, genericPackageName);
         }
     }
 
-    private void mockApplicationEnabledSettingAccessors(IPackageManager pm) throws Exception {
-        doReturn(COMPONENT_ENABLED_STATE_ENABLED).when(pm)
-                .getApplicationEnabledSetting(anyString(), eq(UserHandle.myUserId()));
-
-        doNothing().when(pm).setApplicationEnabledSetting(anyString(), anyInt(),
-                anyInt(), eq(UserHandle.myUserId()), anyString());
+    private void pushLatestIoOveruseStatsAndWait(
+            List<PackageIoOveruseStats> packageIoOveruseStats) throws Exception {
+        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        /* The latestIoOveruseStats call performs resource overuse killing/disabling on the main
+         * thread by posting a new message with RESOURCE_OVERUSE_KILLING_DELAY_MILLS delay. Ensure
+         * this message is processed before returning so the effects of the killing/disabling is
+         * verified.
+         */
+        delayedRunOnMainSync(() -> {}, RESOURCE_OVERUSE_KILLING_DELAY_MILLS * 2);
     }
 
     private void verifyActionsTakenOnResourceOveruse(List<PackageResourceOveruseAction> expected)
@@ -2029,10 +2533,6 @@
                 .containsExactlyElementsIn(expectedStats);
     }
 
-    private static int getUid(int appId) {
-        return UserHandle.getUid(UserHandle.myUserId(), appId);
-    }
-
     private static List<ResourceOveruseConfiguration> sampleResourceOveruseConfigurations() {
         return Arrays.asList(
                 sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
@@ -2057,11 +2557,13 @@
 
     private static ResourceOveruseConfiguration.Builder sampleResourceOveruseConfigurationBuilder(
             int componentType, IoOveruseConfiguration ioOveruseConfig) {
-        String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType);
-        List<String> safeToKill = Arrays.asList(prefix + "_package.A", prefix + "_pkg.B");
+        String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType).toLowerCase();
+        List<String> safeToKill = Arrays.asList(prefix + "_package.non_critical.A",
+                prefix + "_pkg.non_critical.B");
         List<String> vendorPrefixes = Arrays.asList(prefix + "_package", prefix + "_pkg");
         Map<String, String> pkgToAppCategory = new ArrayMap<>();
-        pkgToAppCategory.put(prefix + "_package.A", "android.car.watchdog.app.category.MEDIA");
+        pkgToAppCategory.put(prefix + "_package.non_critical.A",
+                "android.car.watchdog.app.category.MEDIA");
         ResourceOveruseConfiguration.Builder configBuilder =
                 new ResourceOveruseConfiguration.Builder(componentType, safeToKill,
                         vendorPrefixes, pkgToAppCategory);
@@ -2071,28 +2573,28 @@
 
     private static IoOveruseConfiguration.Builder sampleIoOveruseConfigurationBuilder(
             int componentType) {
-        String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType);
+        String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType).toLowerCase();
         PerStateBytes componentLevelThresholds = new PerStateBytes(
-                /* foregroundModeBytes= */10, /* backgroundModeBytes= */20,
-                /* garageModeBytes= */30);
+                /* foregroundModeBytes= */ 10, /* backgroundModeBytes= */ 20,
+                /* garageModeBytes= */ 30);
         Map<String, PerStateBytes> packageSpecificThresholds = new ArrayMap<>();
         packageSpecificThresholds.put(prefix + "_package.A", new PerStateBytes(
-                /* foregroundModeBytes= */40, /* backgroundModeBytes= */50,
-                /* garageModeBytes= */60));
+                /* foregroundModeBytes= */ 40, /* backgroundModeBytes= */ 50,
+                /* garageModeBytes= */ 60));
 
         Map<String, PerStateBytes> appCategorySpecificThresholds = new ArrayMap<>();
         appCategorySpecificThresholds.put(
                 ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MEDIA,
-                new PerStateBytes(/* foregroundModeBytes= */100, /* backgroundModeBytes= */200,
-                        /* garageModeBytes= */300));
+                new PerStateBytes(/* foregroundModeBytes= */ 100, /* backgroundModeBytes= */ 200,
+                        /* garageModeBytes= */ 300));
         appCategorySpecificThresholds.put(
                 ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MAPS,
-                new PerStateBytes(/* foregroundModeBytes= */1100, /* backgroundModeBytes= */2200,
-                        /* garageModeBytes= */3300));
+                new PerStateBytes(/* foregroundModeBytes= */ 1100, /* backgroundModeBytes= */ 2200,
+                        /* garageModeBytes= */ 3300));
 
         List<IoOveruseAlertThreshold> systemWideThresholds = Collections.singletonList(
-                new IoOveruseAlertThreshold(/* durationInSeconds= */10,
-                        /* writtenBytesPerSecond= */200));
+                new IoOveruseAlertThreshold(/* durationInSeconds= */ 10,
+                        /* writtenBytesPerSecond= */ 200));
 
         return new IoOveruseConfiguration.Builder(componentLevelThresholds,
                 packageSpecificThresholds, appCategorySpecificThresholds, systemWideThresholds);
@@ -2101,15 +2603,16 @@
     private static android.automotive.watchdog.internal.ResourceOveruseConfiguration
             sampleInternalResourceOveruseConfiguration(int componentType,
             android.automotive.watchdog.internal.IoOveruseConfiguration ioOveruseConfig) {
-        String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType);
+        String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType).toLowerCase();
         android.automotive.watchdog.internal.ResourceOveruseConfiguration config =
                 new android.automotive.watchdog.internal.ResourceOveruseConfiguration();
         config.componentType = componentType;
-        config.safeToKillPackages = Arrays.asList(prefix + "_package.A", prefix + "_pkg.B");
+        config.safeToKillPackages = Arrays.asList(prefix + "_package.non_critical.A",
+                prefix + "_pkg.non_critical.B");
         config.vendorPackagePrefixes = Arrays.asList(prefix + "_package", prefix + "_pkg");
 
         PackageMetadata metadata = new PackageMetadata();
-        metadata.packageName = prefix + "_package.A";
+        metadata.packageName = prefix + "_package.non_critical.A";
         metadata.appCategoryType = ApplicationCategoryType.MEDIA;
         config.packageMetadata = Collections.singletonList(metadata);
 
@@ -2122,23 +2625,24 @@
 
     private static android.automotive.watchdog.internal.IoOveruseConfiguration
             sampleInternalIoOveruseConfiguration(int componentType) {
-        String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType);
+        String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType).toLowerCase();
         android.automotive.watchdog.internal.IoOveruseConfiguration config =
                 new android.automotive.watchdog.internal.IoOveruseConfiguration();
-        config.componentLevelThresholds = constructPerStateIoOveruseThreshold(prefix,
-                /* fgBytes= */10, /* bgBytes= */20, /* gmBytes= */30);
+        config.componentLevelThresholds = constructPerStateIoOveruseThreshold(
+                WatchdogPerfHandler.toComponentTypeStr(componentType), /* fgBytes= */ 10,
+                /* bgBytes= */ 20, /* gmBytes= */ 30);
         config.packageSpecificThresholds = Collections.singletonList(
-                constructPerStateIoOveruseThreshold(prefix + "_package.A", /* fgBytes= */40,
-                        /* bgBytes= */50, /* gmBytes= */60));
+                constructPerStateIoOveruseThreshold(prefix + "_package.A", /* fgBytes= */ 40,
+                        /* bgBytes= */ 50, /* gmBytes= */ 60));
         config.categorySpecificThresholds = Arrays.asList(
                 constructPerStateIoOveruseThreshold(
                         WatchdogPerfHandler.INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA,
-                        /* fgBytes= */100, /* bgBytes= */200, /* gmBytes= */300),
+                        /* fgBytes= */ 100, /* bgBytes= */ 200, /* gmBytes= */ 300),
                 constructPerStateIoOveruseThreshold(
                         WatchdogPerfHandler.INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS,
-                        /* fgBytes= */1100, /* bgBytes= */2200, /* gmBytes= */3300));
+                        /* fgBytes= */ 1100, /* bgBytes= */ 2200, /* gmBytes= */ 3300));
         config.systemWideThresholds = Collections.singletonList(
-                constructInternalIoOveruseAlertThreshold(/* duration= */10, /* writeBPS= */200));
+                constructInternalIoOveruseAlertThreshold(/* duration= */ 10, /* writeBPS= */ 200));
         return config;
     }
 
@@ -2171,22 +2675,33 @@
         return stats;
     }
 
-    private static ResourceOveruseStats constructResourceOveruseStats(int uid, String packageName,
+    private static ResourceOveruseStats constructResourceOveruseStats(
+            int uid, String packageName, int totalTimesKilled,
             android.automotive.watchdog.IoOveruseStats internalIoOveruseStats) {
-        IoOveruseStats ioOveruseStats =
-                WatchdogPerfHandler.toIoOveruseStatsBuilder(internalIoOveruseStats)
-                        .setKillableOnOveruse(internalIoOveruseStats.killableOnOveruse).build();
+        IoOveruseStats ioOveruseStats = WatchdogPerfHandler.toIoOveruseStatsBuilder(
+                internalIoOveruseStats, totalTimesKilled, internalIoOveruseStats.killableOnOveruse)
+                .build();
 
         return new ResourceOveruseStats.Builder(packageName, UserHandle.getUserHandleForUid(uid))
                 .setIoOveruseStats(ioOveruseStats).build();
     }
 
-    private static android.automotive.watchdog.IoOveruseStats constructInternalIoOveruseStats(
+    private android.automotive.watchdog.IoOveruseStats constructInternalIoOveruseStats(
             boolean killableOnOveruse,
             android.automotive.watchdog.PerStateBytes remainingWriteBytes,
             android.automotive.watchdog.PerStateBytes writtenBytes, int totalOveruses) {
+        return constructInternalIoOveruseStats(killableOnOveruse, STATS_DURATION_SECONDS,
+                remainingWriteBytes, writtenBytes, totalOveruses);
+    }
+
+    private android.automotive.watchdog.IoOveruseStats constructInternalIoOveruseStats(
+            boolean killableOnOveruse, long durationInSecs,
+            android.automotive.watchdog.PerStateBytes remainingWriteBytes,
+            android.automotive.watchdog.PerStateBytes writtenBytes, int totalOveruses) {
         android.automotive.watchdog.IoOveruseStats stats =
                 new android.automotive.watchdog.IoOveruseStats();
+        stats.startTime = mTimeSource.now().getEpochSecond();
+        stats.durationInSeconds = durationInSecs;
         stats.killableOnOveruse = killableOnOveruse;
         stats.remainingWriteBytes = remainingWriteBytes;
         stats.writtenBytes = writtenBytes;
@@ -2194,16 +2709,6 @@
         return stats;
     }
 
-    private static android.automotive.watchdog.PerStateBytes constructPerStateBytes(long fgBytes,
-            long bgBytes, long gmBytes) {
-        android.automotive.watchdog.PerStateBytes perStateBytes =
-                new android.automotive.watchdog.PerStateBytes();
-        perStateBytes.foregroundBytes = fgBytes;
-        perStateBytes.backgroundBytes = bgBytes;
-        perStateBytes.garageModeBytes = gmBytes;
-        return perStateBytes;
-    }
-
     private static PackageResourceOveruseAction constructPackageResourceOveruseAction(
             String packageName, int uid, int[] resourceTypes, int resourceOveruseActionType) {
         PackageResourceOveruseAction action = new PackageResourceOveruseAction();
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/IoOveruseStatsSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/IoOveruseStatsSubject.java
index 48b632f..d5afaf1 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/IoOveruseStatsSubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/IoOveruseStatsSubject.java
@@ -17,28 +17,34 @@
 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 android.car.watchdog.IoOveruseStats;
 
 import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.SimpleSubjectBuilder;
 import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
 
 public final class IoOveruseStatsSubject extends Subject {
-    // Boiler-plate Subject.Factory for IoOveruseStatsSubject
+    /* Boiler-plate Subject.Factory for IoOveruseStatsSubject. */
     private static final Subject.Factory<com.android.car.watchdog.IoOveruseStatsSubject,
             IoOveruseStats> IO_OVERUSE_STATS_SUBJECT_FACTORY =
             com.android.car.watchdog.IoOveruseStatsSubject::new;
 
     private final IoOveruseStats mActual;
 
-    // User-defined entry point
+    /* User-defined entry point. */
     public static IoOveruseStatsSubject assertThat(@Nullable IoOveruseStats stats) {
         return assertAbout(IO_OVERUSE_STATS_SUBJECT_FACTORY).that(stats);
     }
 
-    // Static method for getting the subject factory (for use with assertAbout())
+    public static SimpleSubjectBuilder<IoOveruseStatsSubject, IoOveruseStats> assertWithMessage(
+            String format, Object... args) {
+        return Truth.assertWithMessage(format, args).about(IO_OVERUSE_STATS_SUBJECT_FACTORY);
+    }
+
+    /* Static method for getting the subject factory (for use with assertAbout()). */
     public static Subject.Factory<IoOveruseStatsSubject, IoOveruseStats> ioOveruseStats() {
         return IO_OVERUSE_STATS_SUBJECT_FACTORY;
     }
@@ -49,7 +55,7 @@
         this.mActual = subject;
     }
 
-    // User-defined test assertion SPI below this point
+    /* User-defined test assertion SPI below this point. */
     public void isEqualTo(IoOveruseStats expected) {
         if (mActual == expected) {
             return;
@@ -65,8 +71,8 @@
                 .isEqualTo(expected.getTotalBytesWritten());
         check("isKillableOnOveruse()").that(mActual.isKillableOnOveruse())
                 .isEqualTo(expected.isKillableOnOveruse());
-        assertWithMessage("getRemainingWriteBytes()").about(PerStateBytesSubject.perStateBytes())
-                .that(mActual.getRemainingWriteBytes())
+        Truth.assertWithMessage("getRemainingWriteBytes()")
+                .about(PerStateBytesSubject.perStateBytes()).that(mActual.getRemainingWriteBytes())
                 .isEqualTo(expected.getRemainingWriteBytes());
     }
 
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java
new file mode 100644
index 0000000..e683618
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java
@@ -0,0 +1,165 @@
+/*
+ * 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 android.automotive.watchdog.IoOveruseStats;
+import android.automotive.watchdog.PerStateBytes;
+
+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 IoUsageStatsEntrySubject extends Subject {
+    /* Boiler-plate Subject.Factory for IoUsageStatsEntrySubject. */
+    private static final Subject.Factory<
+            com.android.car.watchdog.IoUsageStatsEntrySubject,
+            Iterable<WatchdogStorage.IoUsageStatsEntry>> IO_OVERUSE_STATS_ENTRY_SUBJECT_FACTORY =
+            com.android.car.watchdog.IoUsageStatsEntrySubject::new;
+    private static final String NULL_ENTRY_STRING = "{NULL}";
+
+    private final Iterable<WatchdogStorage.IoUsageStatsEntry> mActual;
+
+    /* User-defined entry point. */
+    public static IoUsageStatsEntrySubject assertThat(
+            @Nullable Iterable<WatchdogStorage.IoUsageStatsEntry> stats) {
+        return assertAbout(IO_OVERUSE_STATS_ENTRY_SUBJECT_FACTORY).that(stats);
+    }
+
+    public static Subject.Factory<IoUsageStatsEntrySubject,
+            Iterable<WatchdogStorage.IoUsageStatsEntry>> resourceOveruseStats() {
+        return IO_OVERUSE_STATS_ENTRY_SUBJECT_FACTORY;
+    }
+
+    public void containsExactly(WatchdogStorage.IoUsageStatsEntry... stats) {
+        containsExactlyElementsIn(Arrays.asList(stats));
+    }
+
+    public void containsExactlyElementsIn(Iterable<WatchdogStorage.IoUsageStatsEntry> expected) {
+        assertWithMessage("Expected stats(%s) equals to actual stats(%s)", toString(expected),
+                toString(mActual)).that(mActual)
+                .comparingElementsUsing(Correspondence.from(
+                        IoUsageStatsEntrySubject::isEquals, "is equal to"))
+                .containsExactlyElementsIn(expected);
+    }
+
+    public static boolean isEquals(WatchdogStorage.IoUsageStatsEntry actual,
+            WatchdogStorage.IoUsageStatsEntry expected) {
+        if (actual == null || expected == null) {
+            return (actual == null) && (expected == null);
+        }
+        return actual.userId == expected.userId && actual.packageName.equals(expected.packageName)
+                && actual.ioUsage.getTotalTimesKilled() == expected.ioUsage.getTotalTimesKilled()
+                && isEqualsPerStateBytes(actual.ioUsage.getForgivenWriteBytes(),
+                expected.ioUsage.getForgivenWriteBytes())
+                && isEqualsIoOveruseStats(actual.ioUsage.getInternalIoOveruseStats(),
+                expected.ioUsage.getInternalIoOveruseStats());
+    }
+
+    private static boolean isEqualsIoOveruseStats(android.automotive.watchdog.IoOveruseStats actual,
+            android.automotive.watchdog.IoOveruseStats expected) {
+        if (actual == null || expected == null) {
+            return (actual == null) && (expected == null);
+        }
+        return actual.killableOnOveruse == expected.killableOnOveruse
+                && isEqualsPerStateBytes(actual.remainingWriteBytes, expected.remainingWriteBytes)
+                && actual.startTime == expected.startTime
+                && actual.durationInSeconds == expected.durationInSeconds
+                && isEqualsPerStateBytes(actual.writtenBytes, expected.writtenBytes)
+                && actual.totalOveruses == expected.totalOveruses;
+    }
+
+    private static boolean isEqualsPerStateBytes(PerStateBytes actual, PerStateBytes expected) {
+        if (actual == null || expected == null) {
+            return (actual == null) && (expected == null);
+        }
+        return actual.foregroundBytes == expected.foregroundBytes
+                && actual.backgroundBytes == expected.backgroundBytes
+                && actual.garageModeBytes == expected.garageModeBytes;
+    }
+
+    private static String toString(Iterable<WatchdogStorage.IoUsageStatsEntry> entries) {
+        StringBuilder builder = new StringBuilder();
+        builder.append('[');
+        for (WatchdogStorage.IoUsageStatsEntry 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.IoUsageStatsEntry entry) {
+        builder.append("{UserId: ").append(entry.userId)
+                .append(", Package name: ").append(entry.packageName)
+                .append(", IoUsage: ");
+        toStringBuilder(builder, entry.ioUsage);
+        return builder.append('}');
+    }
+
+    private static StringBuilder toStringBuilder(StringBuilder builder,
+            WatchdogPerfHandler.PackageIoUsage ioUsage) {
+        builder.append("{IoOveruseStats: ");
+        toStringBuilder(builder, ioUsage.getInternalIoOveruseStats());
+        builder.append(", ForgivenWriteBytes: ");
+        toStringBuilder(builder, ioUsage.getForgivenWriteBytes());
+        return builder.append(", Total times killed: ").append(ioUsage.getTotalTimesKilled())
+                .append('}');
+    }
+
+    private static StringBuilder toStringBuilder(StringBuilder builder,
+            IoOveruseStats stats) {
+        if (stats == null) {
+            return builder.append(NULL_ENTRY_STRING);
+        }
+        builder.append("{Killable on overuse: ").append(stats.killableOnOveruse)
+                .append(", Remaining write bytes: ");
+        toStringBuilder(builder, stats.remainingWriteBytes);
+        builder.append(", Start time: ").append(stats.startTime)
+                .append(", Duration: ").append(stats.durationInSeconds).append(" seconds")
+                .append(", Total overuses: ").append(stats.totalOveruses)
+                .append(", Written bytes: ");
+        toStringBuilder(builder, stats.writtenBytes);
+        return builder.append('}');
+    }
+
+    private static StringBuilder toStringBuilder(
+            StringBuilder builder, PerStateBytes perStateBytes) {
+        if (perStateBytes == null) {
+            return builder.append(NULL_ENTRY_STRING);
+        }
+        return builder.append("{Foreground bytes: ").append(perStateBytes.foregroundBytes)
+                .append(", Background bytes: ").append(perStateBytes.backgroundBytes)
+                .append(", Garage mode bytes: ").append(perStateBytes.garageModeBytes)
+                .append('}');
+    }
+
+    private IoUsageStatsEntrySubject(FailureMetadata failureMetadata,
+            @Nullable Iterable<WatchdogStorage.IoUsageStatsEntry> iterableSubject) {
+        super(failureMetadata, iterableSubject);
+
+        mActual = iterableSubject;
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseStatsSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseStatsSubject.java
index 776e668..b0ce702 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseStatsSubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseStatsSubject.java
@@ -17,6 +17,7 @@
 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 android.car.watchdog.ResourceOveruseStats;
@@ -42,6 +43,11 @@
         return assertAbout(RESOURCE_OVERUSE_STATS_SUBJECT_FACTORY).that(stats);
     }
 
+    public static void assertEquals(ResourceOveruseStats actual, ResourceOveruseStats expected) {
+        assertWithMessage("Expected stats (%s) equals to actual stats (%s)", expected, actual)
+                .that(isEquals(actual, expected)).isTrue();
+    }
+
     public static Subject.Factory<ResourceOveruseStatsSubject, Iterable<ResourceOveruseStats>>
             resourceOveruseStats() {
         return RESOURCE_OVERUSE_STATS_SUBJECT_FACTORY;
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
new file mode 100644
index 0000000..cca55f6
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/UserPackageSettingsEntrySubject.java
@@ -0,0 +1,98 @@
+/*
+ * 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>> resourceOveruseStats() {
+        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
new file mode 100644
index 0000000..014cc23
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
@@ -0,0 +1,421 @@
+/*
+ * 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 android.car.watchdog.PackageKillableState.KILLABLE_STATE_NEVER;
+import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_NO;
+import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_YES;
+
+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;
+
+import android.automotive.watchdog.PerStateBytes;
+import android.car.watchdog.IoOveruseStats;
+import android.content.Context;
+import android.util.Slog;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * <p>This class contains unit tests for the {@link WatchdogStorage}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class WatchdogStorageUnitTest {
+    private static final String TAG = WatchdogStorageUnitTest.class.getSimpleName();
+
+    private WatchdogStorage mService;
+    private File mDatabaseFile;
+    private TimeSourceInterface mTimeSource;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getTargetContext();
+        mDatabaseFile = context.createDeviceProtectedStorageContext()
+                .getDatabasePath(DATABASE_NAME);
+        mService = new WatchdogStorage(context, /* useDataSystemCarDir= */ false);
+        setDate(/* numDaysAgo= */ 0);
+    }
+
+    @After
+    public void tearDown() {
+        mService.release();
+        if (!mDatabaseFile.delete()) {
+            Slog.e(TAG, "Failed to delete the database file: " + mDatabaseFile.getAbsolutePath());
+        }
+    }
+
+    @Test
+    public void testSaveUserPackageSettings() throws Exception {
+        List<WatchdogStorage.UserPackageSettingsEntry> expected = sampleSettings();
+
+        assertThat(mService.saveUserPackageSettings(expected)).isTrue();
+
+        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+                .containsExactlyElementsIn(expected);
+    }
+
+    @Test
+    public void testOverwriteUserPackageSettings() throws Exception {
+        List<WatchdogStorage.UserPackageSettingsEntry> expected = Arrays.asList(
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 100, "system_package.non_critical.A", KILLABLE_STATE_YES),
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 100, "system_package.non_critical.B", KILLABLE_STATE_NO));
+
+        assertThat(mService.saveUserPackageSettings(expected)).isTrue();
+
+        expected = Arrays.asList(
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 100, "system_package.non_critical.A", KILLABLE_STATE_NEVER),
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 100, "system_package.non_critical.B", KILLABLE_STATE_NO));
+
+        assertThat(mService.saveUserPackageSettings(expected)).isTrue();
+
+        UserPackageSettingsEntrySubject.assertThat(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();
+
+        assertWithMessage("Saved I/O usage stats successfully")
+                .that(mService.saveIoUsageStats(sampleStatsForDate(startTime, /* duration= */ 60)))
+                .isTrue();
+
+        long expectedDuration =
+                mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - startTime;
+        List<WatchdogStorage.IoUsageStatsEntry> expected = sampleStatsForDate(
+                startTime, expectedDuration);
+
+        IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+                .containsExactlyElementsIn(expected);
+    }
+
+    @Test
+    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();
+        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 expectedDuration =
+                mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - expectedStartTime;
+        List<WatchdogStorage.IoUsageStatsEntry> expected = sampleStatsForDate(
+                expectedStartTime, expectedDuration);
+
+        IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+                .containsExactlyElementsIn(expected);
+    }
+
+    @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;
+
+        List<WatchdogStorage.IoUsageStatsEntry> expected = Collections.singletonList(
+                constructIoUsageStatsEntry(
+                        /* userId= */ 100, "system_package.non_critical.A", startTime, duration,
+                        /* remainingWriteBytes= */
+                        CarWatchdogServiceUnitTest.constructPerStateBytes(200, 300, 400),
+                        /* writtenBytes= */
+                        CarWatchdogServiceUnitTest.constructPerStateBytes(1000, 2000, 3000),
+                        /* forgivenWriteBytes= */
+                        CarWatchdogServiceUnitTest.constructPerStateBytes(100, 100, 100),
+                        /* totalOveruses= */ 2, /* totalTimesKilled= */ 1));
+
+        assertWithMessage("Saved I/O usage stats successfully")
+                .that(mService.saveIoUsageStats(expected)).isTrue();
+
+        IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+                .containsExactlyElementsIn(expected);
+
+        expected = Collections.singletonList(
+                constructIoUsageStatsEntry(
+                        /* userId= */ 100, "system_package.non_critical.A", startTime, duration,
+                        /* remainingWriteBytes= */
+                        CarWatchdogServiceUnitTest.constructPerStateBytes(400, 600, 800),
+                        /* writtenBytes= */
+                        CarWatchdogServiceUnitTest.constructPerStateBytes(2000, 3000, 4000),
+                        /* forgivenWriteBytes= */
+                        CarWatchdogServiceUnitTest.constructPerStateBytes(1200, 2300, 3400),
+                        /* totalOveruses= */ 4, /* totalTimesKilled= */ 2));
+
+        assertWithMessage("Saved I/O usage stats successfully")
+                .that(mService.saveIoUsageStats(expected)).isTrue();
+
+        IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+                .containsExactlyElementsIn(expected);
+    }
+
+    @Test
+    public void testSaveIoOveruseStatsOutsideRetentionPeriod() throws Exception {
+        injectSampleUserPackageSettings();
+        int retentionDaysAgo = RETENTION_PERIOD.getDays();
+
+        assertWithMessage("Saved I/O usage stats successfully")
+                .that(mService.saveIoUsageStats(sampleStatsBetweenDates(
+                        /* includingStartDaysAgo= */ retentionDaysAgo,
+                        /* excludingEndDaysAgo= */ retentionDaysAgo + 1))).isTrue();
+
+        assertWithMessage("Didn't fetch I/O overuse stats outside retention period")
+                .that(mService.getHistoricalIoOveruseStats(
+                        /* userId= */ 100, "system_package.non_critical.A", retentionDaysAgo))
+                .isNull();
+    }
+
+    @Test
+    public void testGetHistoricalIoOveruseStats() throws Exception {
+        injectSampleUserPackageSettings();
+
+        assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+                /* includingStartDaysAgo= */ 0, /* excludingEndDaysAgo= */ 5))).isTrue();
+
+        IoOveruseStats actual  = mService.getHistoricalIoOveruseStats(
+                /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
+
+        assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
+
+        /*
+         * 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);
+        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();
+
+        IoOveruseStatsSubject.assertWithMessage(
+                "Fetched stats only for 4 days. Expected stats (%s) equals actual stats (%s)",
+                expected.toString(), actual.toString()).that(actual)
+                .isEqualTo(expected);
+    }
+
+    @Test
+    public void testGetHistoricalIoOveruseStatsWithNoRecentStats() throws Exception {
+        injectSampleUserPackageSettings();
+
+        assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+                /* includingStartDaysAgo= */ 3, /* excludingEndDaysAgo= */ 5))).isTrue();
+
+        IoOveruseStats actual  = mService.getHistoricalIoOveruseStats(
+                /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
+
+        assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
+
+        /*
+         * 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);
+        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();
+
+        IoOveruseStatsSubject.assertWithMessage(
+                "Fetched stats only for 2 days. Expected stats (%s) equals actual stats (%s)",
+                expected.toString(), actual.toString()).that(actual)
+                .isEqualTo(expected);
+    }
+
+    @Test
+    public void testTruncateStatsOutsideRetentionPeriodOnDateChange() throws Exception {
+        injectSampleUserPackageSettings();
+        setDate(/* numDaysAgo= */ 1);
+
+        assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+                /* includingStartDaysAgo= */ 0, /* excludingEndDaysAgo= */ 40),
+                /* shouldCheckRetention= */ false)).isTrue();
+
+        IoOveruseStats actual  = mService.getHistoricalIoOveruseStats(
+                /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 40);
+
+        assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
+
+        ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
+                .truncatedTo(STATS_TEMPORAL_UNIT);
+        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();
+
+        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);
+        mService.shrinkDatabase();
+
+        actual = mService.getHistoricalIoOveruseStats(
+                /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 40);
+
+        assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
+
+        currentDate = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+        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();
+
+        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();
+
+        assertThat(mService.saveUserPackageSettings(expected)).isTrue();
+    }
+
+    private static List<WatchdogStorage.UserPackageSettingsEntry> sampleSettings() {
+        return Arrays.asList(
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 100, "system_package.non_critical.A", KILLABLE_STATE_YES),
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 100, "system_package.non_critical.B", KILLABLE_STATE_NO),
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 100, "vendor_package.critical.C", KILLABLE_STATE_NEVER),
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 101, "system_package.non_critical.A", KILLABLE_STATE_NO),
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 101, "system_package.non_critical.B", KILLABLE_STATE_YES),
+                new WatchdogStorage.UserPackageSettingsEntry(
+                        /* userId= */ 101, "vendor_package.critical.C", KILLABLE_STATE_NEVER));
+    }
+
+    private List<WatchdogStorage.IoUsageStatsEntry> sampleStatsBetweenDates(
+            int includingStartDaysAgo, int excludingEndDaysAgo) {
+        ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
+                .truncatedTo(STATS_TEMPORAL_UNIT);
+        List<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()));
+        }
+        return entries;
+    }
+
+    private static List<WatchdogStorage.IoUsageStatsEntry> sampleStatsForDate(
+            long statsDateEpoch, long duration) {
+        List<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),
+                    /* writtenBytes= */
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(1000, 2000, 3000),
+                    /* forgivenWriteBytes= */
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(100, 100, 100),
+                    /* totalOveruses= */ 2, /* totalTimesKilled= */ 1));
+            entries.add(constructIoUsageStatsEntry(
+                    /* userId= */ i, "vendor_package.critical.C", statsDateEpoch, duration,
+                    /* remainingWriteBytes= */
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(500, 600, 700),
+                    /* writtenBytes= */
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(4000, 5000, 6000),
+                    /* forgivenWriteBytes= */
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(200, 200, 200),
+                    /* totalOveruses= */ 1, /* totalTimesKilled= */ 0));
+        }
+        return entries;
+    }
+
+    private static WatchdogStorage.IoUsageStatsEntry constructIoUsageStatsEntry(
+            int userId, String packageName, long startTime, long duration,
+            PerStateBytes remainingWriteBytes, PerStateBytes writtenBytes,
+            PerStateBytes forgivenWriteBytes, int totalOveruses, int totalTimesKilled) {
+        WatchdogPerfHandler.PackageIoUsage ioUsage = new WatchdogPerfHandler.PackageIoUsage(
+                constructInternalIoOveruseStats(startTime, duration, remainingWriteBytes,
+                        writtenBytes, totalOveruses), forgivenWriteBytes, totalTimesKilled);
+        return new WatchdogStorage.IoUsageStatsEntry(userId, packageName, ioUsage);
+    }
+
+    private static android.automotive.watchdog.IoOveruseStats constructInternalIoOveruseStats(
+            long startTime, long duration, PerStateBytes remainingWriteBytes,
+            PerStateBytes writtenBytes, int totalOveruses) {
+        android.automotive.watchdog.IoOveruseStats stats =
+                new android.automotive.watchdog.IoOveruseStats();
+        stats.startTime = startTime;
+        stats.durationInSeconds = duration;
+        stats.remainingWriteBytes = remainingWriteBytes;
+        stats.writtenBytes = writtenBytes;
+        stats.totalOveruses = totalOveruses;
+        return stats;
+    }
+}
diff --git a/tests/common_utils/src/com/android/car/test/FakeHandlerWrapper.java b/tests/common_utils/src/com/android/car/test/FakeHandlerWrapper.java
new file mode 100644
index 0000000..fe3071f
--- /dev/null
+++ b/tests/common_utils/src/com/android/car/test/FakeHandlerWrapper.java
@@ -0,0 +1,94 @@
+/*
+ * 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.test;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+
+/**
+ * A handler that allows control over when to dispatch messages and callbacks.
+ *
+ * <p>NOTE: Currently only supports {@link Runnable} messages. It doesn't dispatch regular messages.
+ *
+ * <p>Usage: Create an instance of {@link FakeHandlerWrapper}, and use {@link #getMockHandler()}
+ * in your test classes.
+ *
+ * <p>The implementation uses {@link Mockito} to bypass {@code final} keywords.
+ */
+public class FakeHandlerWrapper {
+    private Mode mMode;
+    private ArrayList<Message> mQueuedMessages = new ArrayList<>();
+
+    private final Handler mMockHandler;
+
+    public FakeHandlerWrapper(Looper looper, Mode mode) {
+        mMockHandler = Mockito.spy(new Handler(looper));
+        mMode = mode;
+        // Stubbing #sendMessageAtTime(Message, long).
+        Mockito.doAnswer(invocation -> {
+            Message msg = invocation.getArgument(0);
+            msg.when = invocation.getArgument(1);  // uptimeMillis
+            mQueuedMessages.add(msg);
+            if (mMode == Mode.IMMEDIATE) {
+                dispatchQueuedMessages();
+            }
+            return true;
+        }).when(mMockHandler).sendMessageAtTime(Mockito.any(), Mockito.anyLong());
+        // Stubbing #removeCallbacks(Runnable).
+        Mockito.doAnswer(invocation -> {
+            Runnable callback = invocation.getArgument(0);
+            return mQueuedMessages.removeIf(msg -> msg.getCallback() == callback);
+        }).when(mMockHandler).removeCallbacks(Mockito.any());
+    }
+
+    public Handler getMockHandler() {
+        return mMockHandler;
+    }
+
+    public void setMode(Mode mode) {
+        mMode = mode;
+    }
+
+    /** Dispatch any messages that have been queued on the calling thread. */
+    public void dispatchQueuedMessages() {
+        ArrayList<Message> messages = new ArrayList<>(mQueuedMessages);
+        mQueuedMessages.clear();
+        for (Message msg : messages) {
+            Runnable callback = msg.getCallback();
+            if (callback != null) {
+                callback.run();
+            }
+        }
+    }
+
+    /** Returns the queued messages list. */
+    public ArrayList<Message> getQueuedMessages() {
+        return new ArrayList<>(mQueuedMessages);
+    }
+
+    public enum Mode {
+        /** Messages are dispatched immediately on the calling thread. */
+        IMMEDIATE,
+        /** Messages are queued until {@link #dispatchQueuedMessages()} is called. */
+        QUEUEING,
+    }
+}