[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: 695cd98c78 -s ours am: 7fd2d64dd1 -s ours am: 8e77872eaa -s ours

am skip reason: subject contains skip directive

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Car/+/15894793

Change-Id: I6b4287a1a7acdc2721ec0333dacdb4f410483637
diff --git a/Android.mk b/Android.mk
index 8b1efbf..0d59692 100644
--- a/Android.mk
+++ b/Android.mk
@@ -15,5 +15,8 @@
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
+# Include car_ui_portrait
+include $(LOCAL_PATH)/car_product/car_ui_portrait/Android.mk
+
 # Include the sub-makefiles
 include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index aa98215..23ed24d 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -904,6 +904,25 @@
     public static final String CAR_EXTRA_BROWSE_SERVICE_FOR_SESSION =
             "android.media.session.BROWSE_SERVICE";
 
+    /**
+     * If some specific Activity should be launched on the designated TDA all the time, include this
+     * integer extra in the first launching Intent and ActivityOption with the launch TDA.
+     * If the value is {@link #LAUNCH_PERSISTENT_ADD}, CarLaunchParamsModifier will memorize
+     * the Activity and the TDA pair, and assign the TDA in the following Intents for the Activity.
+     * If there is any assigned Activity on the TDA, it'll be replaced with the new Activity.
+     * If the value is {@Link #LAUNCH_PERSISTENT_DELETE}, it'll remove the stored info for the given
+     * Activity.
+     *
+     * @hide
+     */
+    public static final String CAR_EXTRA_LAUNCH_PERSISTENT =
+            "android.car.intent.extra.launchparams.PERSISTENT";
+
+    /** @hide */
+    public static final int LAUNCH_PERSISTENT_DELETE = 0;
+    /** @hide */
+    public static final int LAUNCH_PERSISTENT_ADD = 1;
+
     /** @hide */
     public static final String CAR_SERVICE_INTERFACE_NAME = CommonConstants.CAR_SERVICE_INTERFACE;
 
diff --git a/car-lib/src/android/car/CarAppFocusManager.java b/car-lib/src/android/car/CarAppFocusManager.java
index cc0d10b..7a6d1f3 100644
--- a/car-lib/src/android/car/CarAppFocusManager.java
+++ b/car-lib/src/android/car/CarAppFocusManager.java
@@ -17,6 +17,7 @@
 package android.car;
 
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -28,6 +29,7 @@
 import java.lang.ref.WeakReference;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -221,6 +223,22 @@
     }
 
     /**
+     * Returns the package names of the current owner of a given application type, or {@code null}
+     * if there is no owner. This method might return more than one package name if the current
+     * owner uses the "android:sharedUserId" feature.
+     *
+     * @hide
+     */
+    @Nullable
+    public List<String> getAppTypeOwner(@AppFocusType int appType) {
+        try {
+            return mService.getAppTypeOwner(appType);
+        } catch (RemoteException e) {
+            return handleRemoteExceptionFromCarService(e, null);
+        }
+    }
+
+    /**
      * Checks if listener is associated with active a focus
      * @param callback
      * @param appType
diff --git a/car-lib/src/android/car/IAppFocus.aidl b/car-lib/src/android/car/IAppFocus.aidl
index f3d6d1f..118ad3b 100644
--- a/car-lib/src/android/car/IAppFocus.aidl
+++ b/car-lib/src/android/car/IAppFocus.aidl
@@ -30,4 +30,5 @@
     int requestAppFocus(IAppFocusOwnershipCallback callback, int appType) = 4;
     /** callback used as a token */
     void abandonAppFocus(IAppFocusOwnershipCallback callback, int appType) = 5;
+    List<String> getAppTypeOwner(int appType) = 6;
 }
diff --git a/car-lib/src/android/car/ICarUserService.aidl b/car-lib/src/android/car/ICarUserService.aidl
index 267044d..bee3bba 100644
--- a/car-lib/src/android/car/ICarUserService.aidl
+++ b/car-lib/src/android/car/ICarUserService.aidl
@@ -40,8 +40,8 @@
     List<UserInfo> getPassengers(int driverId);
     boolean startPassenger(int passengerId, int zoneId);
     boolean stopPassenger(int passengerId);
-    void setLifecycleListenerForUid(in IResultReceiver listener);
-    void resetLifecycleListenerForUid();
+    void setLifecycleListenerForApp(String pkgName, in IResultReceiver listener);
+    void resetLifecycleListenerForApp(in IResultReceiver listener);
     UserIdentificationAssociationResponse getUserIdentificationAssociation(in int[] types);
     void setUserIdentificationAssociation(int timeoutMs, in int[] types, in int[] values,
       in AndroidFuture<UserIdentificationAssociationResponse> result);
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index fe697b1..008fc4b 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -23,6 +23,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.annotation.UserIdInt;
+import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.Service;
 import android.car.Car;
@@ -152,7 +153,7 @@
                 String packageName) {
             try {
                 ProviderInfo[] providers = packageManager.getPackageInfo(packageName,
-                        PackageManager.GET_PROVIDERS).providers;
+                        PackageManager.GET_PROVIDERS | PackageManager.MATCH_ANY_USER).providers;
                 if (providers == null) {
                     return Collections.emptyList();
                 }
@@ -370,8 +371,16 @@
         }
     }
 
+    /**
+     * Returns the cluster activity from the application given by its package name.
+     *
+     * @return the {@link ComponentName} of the cluster activity, or null if the given application
+     * doesn't have a cluster activity.
+     *
+     * @hide
+     */
     @Nullable
-    private ComponentName getComponentFromPackage(@NonNull String packageName) {
+    public ComponentName getComponentFromPackage(@NonNull String packageName) {
         PackageManager packageManager = getPackageManager();
 
         // Check package permission.
@@ -385,8 +394,8 @@
         Intent intent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Car.CAR_CATEGORY_NAVIGATION)
                 .setPackage(packageName);
-        List<ResolveInfo> resolveList = packageManager.queryIntentActivities(intent,
-                PackageManager.GET_RESOLVED_FILTER);
+        List<ResolveInfo> resolveList = packageManager.queryIntentActivitiesAsUser(intent,
+                PackageManager.GET_RESOLVED_FILTER, ActivityManager.getCurrentUser());
         if (resolveList == null || resolveList.isEmpty()
                 || resolveList.get(0).getComponentInfo() == null) {
             Log.i(TAG, "Failed to resolve an intent: " + intent);
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..b0067b1 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,77 @@
     }
 
     /**
-     * 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}. This
+     * will also remove outputs produced by the MetricsConfig. 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}. This
+     * will also remove all MetricsConfig outputs.
      *
      * @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 +385,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 +399,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..8343c34 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,29 @@
     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. This will also remove outputs produced by the
+     * MetricsConfig.
      */
-    boolean removeManifest(in ManifestKey key);
+    void removeMetricsConfig(in MetricsConfigKey key);
 
     /**
-     * Removes all manifests.
+     * Removes all MetricsConfigs. This will also remove all MetricsConfig outputs.
      */
-    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/IScriptExecutor.aidl b/car-lib/src/android/car/telemetry/IScriptExecutor.aidl
deleted file mode 100644
index 83ea3f0..0000000
--- a/car-lib/src/android/car/telemetry/IScriptExecutor.aidl
+++ /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.
- */
-
-package android.car.telemetry;
-
-import android.car.telemetry.IScriptExecutorListener;
-import android.os.Bundle;
-
-/**
- * An internal API provided by isolated Script Executor process
- * for executing Lua scripts in a sandboxed environment
- *
- * @hide
- */
-interface IScriptExecutor {
-  /**
-   * Executes a specified function in provided Lua script with given input arguments.
-   *
-   * @param scriptBody complete body of Lua script that also contains the function to be invoked
-   * @param functionName the name of the function to execute
-   * @param publishedData input data provided by the source which the function handles
-   * @param savedState key-value pairs preserved from the previous invocation of the function
-   * @param listener callback for the sandboxed environent to report back script execution results, errors, and logs
-   */
-  void invokeScript(String scriptBody,
-                    String functionName,
-                    in Bundle publishedData,
-                    in @nullable Bundle savedState,
-                    in IScriptExecutorListener listener);
-}
diff --git a/car-lib/src/android/car/telemetry/IScriptExecutorListener.aidl b/car-lib/src/android/car/telemetry/IScriptExecutorListener.aidl
deleted file mode 100644
index d751a61..0000000
--- a/car-lib/src/android/car/telemetry/IScriptExecutorListener.aidl
+++ /dev/null
@@ -1,73 +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.telemetry;
-
-import android.os.Bundle;
-
-/**
- * Listener for {@code IScriptExecutor#invokeScript}.
- *
- * An invocation of a script by Script Executor will result in a call of only one
- * of the three methods below. If a script fully completes its objective, onScriptFinished
- * is called. If a script's invocation completes normally, onSuccess is called.
- * onError is called if any error happens before or during script execution and we
- * should abandon this run of the script.
- */
-interface IScriptExecutorListener {
-  /**
-   * Called by ScriptExecutor when the script declares itself as "finished".
-   *
-   * @param result final results of the script that will be uploaded.
-   */
-  void onScriptFinished(in byte[] result);
-
-  /**
-   * Called by ScriptExecutor when a function completes successfully and also provides
-   * optional state that the script wants CarTelemetryService to persist.
-   *
-   * @param stateToPersist key-value pairs to persist
-   */
-  void onSuccess(in @nullable Bundle stateToPersist);
-
-  /**
-   * Default error type.
-   */
-  const int ERROR_TYPE_UNSPECIFIED = 0;
-
-  /**
-   * Used when an error occurs in the ScriptExecutor code.
-   */
-  const int ERROR_TYPE_SCRIPT_EXECUTOR_ERROR = 1;
-
-  /**
-   * Used when an error occurs while executing the Lua script (such as
-   * errors returned by lua_pcall)
-   */
-  const int ERROR_TYPE_LUA_RUNTIME_ERROR = 2;
-
-
-  /**
-   * Called by ScriptExecutor to report errors that prevented the script
-   * from running or completing execution successfully.
-   *
-   * @param errorType type of the error message as defined in this aidl file.
-   * @param messsage the human-readable message containing information helpful for analysis or debugging.
-   * @param stackTrace the stack trace of the error if available.
-   */
-  void onError(int errorType, String message, @nullable String stackTrace);
-}
-
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.aidl b/car-lib/src/android/car/telemetry/ManifestKey.aidl
deleted file mode 100644
index 25097df..0000000
--- a/car-lib/src/android/car/telemetry/ManifestKey.aidl
+++ /dev/null
@@ -1,22 +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.telemetry;
-
-/**
- * @hide
- */
-parcelable ManifestKey;
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.java b/car-lib/src/android/car/telemetry/ManifestKey.java
deleted file mode 100644
index b0a69c2..0000000
--- a/car-lib/src/android/car/telemetry/ManifestKey.java
+++ /dev/null
@@ -1,76 +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.telemetry;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * A parcelable that wraps around the Manifest name and version.
- *
- * @hide
- */
-public final class ManifestKey implements Parcelable {
-
-    @NonNull
-    private String mName;
-    private int mVersion;
-
-    @NonNull
-    public String getName() {
-        return mName;
-    }
-
-    public int getVersion() {
-        return mVersion;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel out, int flags) {
-        out.writeString(mName);
-        out.writeInt(mVersion);
-    }
-
-    private ManifestKey(Parcel in) {
-        mName = in.readString();
-        mVersion = in.readInt();
-    }
-
-    public ManifestKey(@NonNull String name, int version) {
-        mName = name;
-        mVersion = version;
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    public static final @NonNull Parcelable.Creator<ManifestKey> CREATOR =
-            new Parcelable.Creator<ManifestKey>() {
-                @Override
-                public ManifestKey createFromParcel(Parcel in) {
-                    return new ManifestKey(in);
-                }
-
-                @Override
-                public ManifestKey[] newArray(int size) {
-                    return new ManifestKey[size];
-                }
-            };
-}
diff --git a/car-lib/src/android/car/telemetry/MetricsConfigKey.aidl b/car-lib/src/android/car/telemetry/MetricsConfigKey.aidl
new file mode 100644
index 0000000..2c00127
--- /dev/null
+++ b/car-lib/src/android/car/telemetry/MetricsConfigKey.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.telemetry;
+
+/**
+ * @hide
+ */
+parcelable MetricsConfigKey;
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/MetricsConfigKey.java b/car-lib/src/android/car/telemetry/MetricsConfigKey.java
new file mode 100644
index 0000000..ddc9718
--- /dev/null
+++ b/car-lib/src/android/car/telemetry/MetricsConfigKey.java
@@ -0,0 +1,92 @@
+/*
+ * 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.telemetry;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A parcelable that wraps around the Manifest name and version.
+ *
+ * @hide
+ */
+public final class MetricsConfigKey implements Parcelable {
+
+    @NonNull
+    private String mName;
+    private int mVersion;
+
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    public int getVersion() {
+        return mVersion;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeString(mName);
+        out.writeInt(mVersion);
+    }
+
+    private MetricsConfigKey(Parcel in) {
+        mName = in.readString();
+        mVersion = in.readInt();
+    }
+
+    public MetricsConfigKey(@NonNull String name, int version) {
+        mName = name;
+        mVersion = version;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof MetricsConfigKey)) {
+            return false;
+        }
+        MetricsConfigKey other = (MetricsConfigKey) o;
+        return mName.equals(other.getName()) && mVersion == other.getVersion();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mVersion);
+    }
+
+    public static final @NonNull Parcelable.Creator<MetricsConfigKey> CREATOR =
+            new Parcelable.Creator<MetricsConfigKey>() {
+                @Override
+                public MetricsConfigKey createFromParcel(Parcel in) {
+                    return new MetricsConfigKey(in);
+                }
+
+                @Override
+                public MetricsConfigKey[] newArray(int size) {
+                    return new MetricsConfigKey[size];
+                }
+            };
+}
diff --git a/car-lib/src/android/car/user/CarUserManager.java b/car-lib/src/android/car/user/CarUserManager.java
index dace5a8..f42da45 100644
--- a/car-lib/src/android/car/user/CarUserManager.java
+++ b/car-lib/src/android/car/user/CarUserManager.java
@@ -307,13 +307,20 @@
     public @interface UserIdentificationAssociationValue{}
 
     private final Object mLock = new Object();
+
     private final ICarUserService mService;
     private final UserManager mUserManager;
 
+    /**
+     * Map of listeners registers by the app.
+     */
     @Nullable
     @GuardedBy("mLock")
     private ArrayMap<UserLifecycleListener, Executor> mListeners;
 
+    /**
+     * Receiver used to receive user-lifecycle callbacks from the service.
+     */
     @Nullable
     @GuardedBy("mLock")
     private LifecycleResultReceiver mReceiver;
@@ -332,6 +339,7 @@
     public CarUserManager(@NonNull Car car, @NonNull ICarUserService service,
             @NonNull UserManager userManager) {
         super(car);
+
         mService = service;
         mUserManager = userManager;
     }
@@ -524,20 +532,31 @@
         Objects.requireNonNull(listener, "listener cannot be null");
 
         int uid = myUid();
+        String packageName = getContext().getPackageName();
+        if (DBG) {
+            Log.d(TAG, "addListener(): uid=" + uid + ", pkg=" + packageName
+                    + ", listener=" + listener);
+        }
         synchronized (mLock) {
             Preconditions.checkState(mListeners == null || !mListeners.containsKey(listener),
                     "already called for this listener");
             if (mReceiver == null) {
                 mReceiver = new LifecycleResultReceiver();
                 try {
-                    EventLog.writeEvent(EventLogTags.CAR_USER_MGR_ADD_LISTENER, uid);
-                    if (DBG) Log.d(TAG, "Setting lifecycle receiver for uid " + uid);
-                    mService.setLifecycleListenerForUid(mReceiver);
+                    EventLog.writeEvent(EventLogTags.CAR_USER_MGR_ADD_LISTENER, uid, packageName);
+                    if (DBG) {
+                        Log.d(TAG, "Setting lifecycle receiver for uid " + uid + " and package "
+                                + packageName);
+                    }
+                    mService.setLifecycleListenerForApp(packageName, mReceiver);
                 } catch (RemoteException e) {
                     handleRemoteExceptionFromCarService(e);
                 }
             } else {
-                if (DBG) Log.d(TAG, "Already set receiver for uid " + uid);
+                if (DBG) {
+                    Log.d(TAG, "Already set receiver for uid " + uid + " and package "
+                            + packageName);
+                }
             }
 
             if (mListeners == null) {
@@ -547,7 +566,7 @@
                         + " already has " + mListeners.size() + " listeners: "
                         + mListeners.keySet().stream()
                                 .map((l) -> getLambdaName(l))
-                                .collect(Collectors.toList()), new Exception());
+                                .collect(Collectors.toList()), new Exception("caller's stack"));
             }
             if (DBG) Log.d(TAG, "Adding listener: " + listener);
             mListeners.put(listener, executor);
@@ -568,6 +587,11 @@
         Objects.requireNonNull(listener, "listener cannot be null");
 
         int uid = myUid();
+        String packageName = getContext().getPackageName();
+        if (DBG) {
+            Log.d(TAG, "removeListener(): uid=" + uid + ", pkg=" + packageName
+                    + ", listener=" + listener);
+        }
         synchronized (mLock) {
             Preconditions.checkState(mListeners != null && mListeners.containsKey(listener),
                     "not called for this listener yet");
@@ -584,10 +608,13 @@
                 return;
             }
 
-            EventLog.writeEvent(EventLogTags.CAR_USER_MGR_REMOVE_LISTENER, uid);
-            if (DBG) Log.d(TAG, "Removing lifecycle receiver for uid=" + uid);
+            EventLog.writeEvent(EventLogTags.CAR_USER_MGR_REMOVE_LISTENER, uid, packageName);
+            if (DBG) {
+                Log.d(TAG, "Removing lifecycle receiver for uid=" + uid + " and package "
+                        + packageName);
+            }
             try {
-                mService.resetLifecycleListenerForUid();
+                mService.resetLifecycleListenerForApp(mReceiver);
                 mReceiver = null;
             } catch (RemoteException e) {
                 handleRemoteExceptionFromCarService(e);
diff --git a/car-lib/src/android/car/watchdog/CarWatchdogManager.java b/car-lib/src/android/car/watchdog/CarWatchdogManager.java
index aa930a0..007bb22 100644
--- a/car-lib/src/android/car/watchdog/CarWatchdogManager.java
+++ b/car-lib/src/android/car/watchdog/CarWatchdogManager.java
@@ -410,8 +410,8 @@
     /**
      * Returns resource overuse stats for a specific user package.
      *
-     * @param packageName Name of the package whose stats should to be returned.
-     * @param userId ID of the user whose stats should be returned.
+     * @param packageName Name of the package whose stats should be returned.
+     * @param userHandle Handle of the user whose stats should be returned.
      * @param resourceOveruseFlag Flag to indicate the types of resource overuse stats to return.
      * @param maxStatsPeriod Maximum period to aggregate the resource overuse stats.
      *
@@ -635,7 +635,10 @@
      * exception. This API may be used by CarSettings application or UI notification.
      *
      * @param packageName Name of the package whose setting should to be updated.
-     * @param userHandle  User whose setting should to be updated.
+     *                    Note: All packages under shared UID share the killable state as well. Thus
+     *                    setting the killable state for one package will set the killable state for
+     *                    all other packages that share a UID.
+     * @param userHandle  User whose setting should be updated.
      * @param isKillable  Whether or not the package for the specified user is killable on resource
      *                    overuse.
      *
@@ -657,7 +660,7 @@
      *
      * <p>This API may be used by CarSettings application or UI notification.
      *
-     * @param userHandle User whose killable states for all packages should to be returned.
+     * @param userHandle User whose killable states for all packages should be returned.
      *
      * @hide
      */
diff --git a/car-lib/src/android/car/watchdog/IoOveruseConfiguration.java b/car-lib/src/android/car/watchdog/IoOveruseConfiguration.java
index 608c3c3..2e23d46 100644
--- a/car-lib/src/android/car/watchdog/IoOveruseConfiguration.java
+++ b/car-lib/src/android/car/watchdog/IoOveruseConfiguration.java
@@ -48,6 +48,10 @@
     /**
      * Package specific thresholds only for system and vendor packages.
      *
+     * NOTE: For packages that share a UID, the package name should be the shared package name
+     * because the I/O usage is aggregated for all packages under the shared UID. The shared
+     * package names should have the prefix 'shared:'.
+     *
      * <p>System component must provide package specific thresholds only for system packages.
      * <p>Vendor component must provide package specific thresholds only for vendor packages.
      */
diff --git a/car-lib/src/android/car/watchdog/ResourceOveruseConfiguration.java b/car-lib/src/android/car/watchdog/ResourceOveruseConfiguration.java
index 03a972a..a411a0d 100644
--- a/car-lib/src/android/car/watchdog/ResourceOveruseConfiguration.java
+++ b/car-lib/src/android/car/watchdog/ResourceOveruseConfiguration.java
@@ -76,8 +76,10 @@
     /**
      * List of system or vendor packages that are safe to be killed on resource overuse.
      *
-     * <p>System component must provide only safe-to-kill system packages in this list.
-     * <p>Vendor component must provide only safe-to-kill vendor packages in this list.
+     * <p>When specifying shared package names, the package names should contain the prefix
+     * 'shared:'.
+     * <p>System components must provide only safe-to-kill system packages in this list.
+     * <p>Vendor components must provide only safe-to-kill vendor packages in this list.
      */
     private @NonNull List<String> mSafeToKillPackages;
 
@@ -87,6 +89,9 @@
      * <p>Any pre-installed package name starting with one of the prefixes or any package from the
      * vendor partition is identified as a vendor package and vendor provided thresholds are applied
      * to these packages. This list must be provided only by the vendor component.
+     *
+     * <p>When specifying shared package name prefixes, the prefix should contain 'shared:' at
+     * the beginning.
      */
     private @NonNull List<String> mVendorPackagePrefixes;
 
@@ -97,6 +102,10 @@
      * <p>This mapping must contain only packages that can be mapped to one of the
      * {@link ApplicationCategoryType} types. This mapping must be defined only by the system and
      * vendor components.
+     *
+     * <p>For packages under a shared UID, the application category type must be specified
+     * for the shared package name and not for individual packages under the shared UID. When
+     * specifying shared package names, the package names should contain the prefix 'shared:'.
      */
     private @NonNull Map<String, String> mPackagesToAppCategoryTypes;
 
diff --git a/car-lib/src/android/car/watchdog/ResourceOveruseStats.java b/car-lib/src/android/car/watchdog/ResourceOveruseStats.java
index 992cff2..d9b639f 100644
--- a/car-lib/src/android/car/watchdog/ResourceOveruseStats.java
+++ b/car-lib/src/android/car/watchdog/ResourceOveruseStats.java
@@ -33,6 +33,9 @@
 public final class ResourceOveruseStats implements Parcelable {
     /**
      * Name of the package, whose stats are recorded in the below fields.
+     *
+     * NOTE: For packages that share a UID, the package name will be the shared package name because
+     *       the stats are aggregated for all packages under the shared UID.
      */
     private @NonNull String mPackageName;
 
diff --git a/car-lib/src/com/android/car/internal/common/EventLogTags.logtags b/car-lib/src/com/android/car/internal/common/EventLogTags.logtags
index 65a3fb1..45fdfe9 100644
--- a/car-lib/src/com/android/car/internal/common/EventLogTags.logtags
+++ b/car-lib/src/com/android/car/internal/common/EventLogTags.logtags
@@ -69,8 +69,8 @@
 150100 car_user_svc_initial_user_info_req (request_type|1),(timeout|1)
 150101 car_user_svc_initial_user_info_resp (status|1),(action|1),(user_id|1),(flags|1),(safe_name|3),(user_locales|3)
 150103 car_user_svc_set_initial_user (user_id|1)
-150104 car_user_svc_set_lifecycle_listener (uid|1)
-150105 car_user_svc_reset_lifecycle_listener (uid|1)
+150104 car_user_svc_set_lifecycle_listener (uid|1),(package_name|3)
+150105 car_user_svc_reset_lifecycle_listener (uid|1),(package_name|3)
 150106 car_user_svc_switch_user_req (user_id|1),(timeout|1)
 150107 car_user_svc_switch_user_resp (hal_callback_status|1),(user_switch_status|1),(error_message|3)
 150108 car_user_svc_post_switch_user_req (target_user_id|1),(current_user_id|1)
@@ -86,7 +86,7 @@
 150118 car_user_svc_create_user_user_removed (user_id|1),(reason|3)
 150119 car_user_svc_remove_user_req (user_id|1),(hasCallerRestrictions|1)
 150120 car_user_svc_remove_user_resp (user_id|1),(result|1)
-150121 car_user_svc_notify_app_lifecycle_listener (uid|1),(event_type|1),(from_user_id|1),(to_user_id|1)
+150121 car_user_svc_notify_app_lifecycle_listener (uid|1),(package_name|3),(event_type|1),(from_user_id|1),(to_user_id|1)
 150122 car_user_svc_notify_internal_lifecycle_listener (listener_name|3),(event_type|1),(from_user_id|1),(to_user_id|1)
 150123 car_user_svc_pre_creation_requested (number_users|1),(number_guests|1)
 150124 car_user_svc_pre_creation_status (number_existing_users|1),(number_users_to_add|1),(number_users_to_remove|1),(number_existing_guests|1),(number_guests_to_add|1),(number_guests_to_remove|1),(number_invalid_users_to_remove|1)
@@ -111,8 +111,8 @@
 150152 car_user_hal_create_user_resp (request_id|1),(status|1),(result|1),(error_message|3)
 150153 car_user_hal_remove_user_req (target_user_id|1),(current_user_id|1)
 
-150171 car_user_mgr_add_listener (uid|1)
-150172 car_user_mgr_remove_listener (uid|1)
+150171 car_user_mgr_add_listener (uid|1),(package_name|3)
+150172 car_user_mgr_remove_listener (uid|1),(package_name|3)
 150173 car_user_mgr_disconnected (uid|1)
 150174 car_user_mgr_switch_user_req (uid|1),(user_id|1)
 150175 car_user_mgr_switch_user_resp (uid|1),(status|1),(error_message|3)
diff --git a/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java b/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java
index deb3946..29b1471 100644
--- a/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java
+++ b/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java
@@ -50,6 +50,7 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 
 /**
  * Provides common Mockito calls for core Android classes.
@@ -133,6 +134,16 @@
     }
 
     /**
+     * Mocks {@code UserManager#getUserHandles()} to return the simple users with the given ids.
+     */
+    public static void mockUmGetUserHandles(@NonNull UserManager um, boolean excludeDying,
+            @NonNull @UserIdInt int... userIds) {
+        List<UserHandle> result = UserTestingHelper.newUsers(userIds).stream().map(
+                UserInfo::getUserHandle).collect(Collectors.toList());
+        when(um.getUserHandles(excludeDying)).thenReturn(result);
+    }
+
+    /**
      * Mocks {@code UserManager#getUsers(excludePartial, excludeDying, excludeDying)} to return the
      * given users.
      */
diff --git a/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java b/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
index 627aa86..3693ebc 100644
--- a/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
+++ b/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
@@ -54,6 +54,8 @@
 
     private static final long DEFAULT_TIMEOUT_MS = 2_000;
 
+    private static int sNextId;
+
     private final Object mLock = new Object();
 
     private final CountDownLatch mLatch = new CountDownLatch(1);
@@ -79,6 +81,8 @@
 
     private final long mTimeoutMs;
 
+    private final int mId = ++sNextId;
+
     private BlockingUserLifecycleListener(Builder builder) {
         mExpectedEventTypes = Collections
                 .unmodifiableList(new ArrayList<>(builder.mExpectedEventTypes));
@@ -276,7 +280,7 @@
     @NonNull
     private String stateToString() {
         synchronized (mLock) {
-            return "timeout=" + mTimeoutMs + "ms"
+            return "id=" + mId + ",timeout=" + mTimeoutMs + "ms"
                     + ",expectedEventTypes=" + toString(mExpectedEventTypes)
                     + ",expectedEventTypesLeft=" + toString(mExpectedEventTypesLeft)
                     + (expectingSpecificUser() ? ",forUser=" + mForUserId : "")
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/build/car.mk b/car_product/build/car.mk
index df09aae..67202f2 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -45,6 +45,7 @@
     BugReportApp \
     NetworkPreferenceApp \
     SampleCustomInputService \
+    AdasLocationTestApp \
 
 # SEPolicy for test apps / services
 BOARD_SEPOLICY_DIRS += packages/services/Car/car_product/sepolicy/test
@@ -128,6 +129,7 @@
     car-frameworks-service \
     com.android.car.procfsinspector \
     libcar-framework-service-jni \
+    ScriptExecutor \
 
 # RROs
 PRODUCT_PACKAGES += \
diff --git a/car_product/build/car_base.mk b/car_product/build/car_base.mk
index ecd503b..4d3cf5e 100644
--- a/car_product/build/car_base.mk
+++ b/car_product/build/car_base.mk
@@ -61,6 +61,7 @@
     A2dpSinkService \
     PackageInstaller \
     carbugreportd \
+    vehicle_binding_util \
 
 # ENABLE_CAMERA_SERVICE must be set as true from the product's makefile if it wants to support
 # Android Camera service.
@@ -82,8 +83,7 @@
 include packages/services/Car/cpp/evs/sampleDriver/sepolicy/evsdriver.mk
 endif
 ifeq ($(ENABLE_CAREVSSERVICE_SAMPLE), true)
-PRODUCT_PACKAGES += CarEvsCameraPreviewApp \
-                    CarSystemUIEvsRRO
+PRODUCT_PACKAGES += CarEvsCameraPreviewApp
 endif
 ifeq ($(ENABLE_REAR_VIEW_CAMERA_SAMPLE), true)
 PRODUCT_PACKAGES += SampleRearViewCamera
@@ -109,6 +109,10 @@
     packages/services/Car/car_product/init/init.bootstat.rc:system/etc/init/init.bootstat.car.rc \
     packages/services/Car/car_product/init/init.car.rc:system/etc/init/init.car.rc
 
+# Device policy management support
+PRODUCT_COPY_FILES += \
+    frameworks/native/data/etc/android.software.device_admin.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.device_admin.xml
+
 # Enable car watchdog
 include packages/services/Car/cpp/watchdog/product/carwatchdog.mk
 
diff --git a/car_product/build/preinstalled-packages-product-car-base.xml b/car_product/build/preinstalled-packages-product-car-base.xml
index f8ffbc1..b241c89 100644
--- a/car_product/build/preinstalled-packages-product-car-base.xml
+++ b/car_product/build/preinstalled-packages-product-car-base.xml
@@ -271,6 +271,11 @@
     <install-in-user-type package="com.android.bluetoothmidiservice">
         <install-in user-type="FULL" />
     </install-in-user-type>
+    <!-- ManagedProvisioning app is used for provisioning the device. It
+         requires UX for the provisioning flow. -->
+    <install-in-user-type package="com.android.managedprovisioning">
+        <install-in user-type="FULL" />
+    </install-in-user-type>
     <install-in-user-type package="com.android.statementservice">
         <install-in user-type="FULL" />
     </install-in-user-type>
diff --git a/car_product/car_ui_portrait/Android.mk b/car_product/car_ui_portrait/Android.mk
new file mode 100644
index 0000000..53ad94b
--- /dev/null
+++ b/car_product/car_ui_portrait/Android.mk
@@ -0,0 +1,21 @@
+# 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.
+#
+
+car_ui_portrait_modules := \
+    rro/car-ui-customizations \
+    rro/car-ui-toolbar-customizations \
+    apps/HideApps
+
+include $(call all-named-subdir-makefiles,$(car_ui_portrait_modules))
diff --git a/car_product/car_ui_portrait/OWNERS b/car_product/car_ui_portrait/OWNERS
new file mode 100644
index 0000000..f539bfb
--- /dev/null
+++ b/car_product/car_ui_portrait/OWNERS
@@ -0,0 +1,6 @@
+# Car UI Portrait Reference OWNERS
+hseog@google.com
+priyanksingh@google.com
+juliakawano@google.com
+stenning@google.com
+igorr@google.com
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/Android.bp b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/Android.bp
new file mode 100644
index 0000000..3c5fb85
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/Android.bp
@@ -0,0 +1,53 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "CarUiPortraitSettings",
+    overrides: ["CarSettings"],
+    platform_apis: true,
+
+    manifest: "AndroidManifest.xml",
+
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "CarSettings-core",
+    ],
+
+    certificate: "platform",
+
+    optimize: {
+        enabled: false,
+    },
+
+    privileged: true,
+
+    dex_preopt: {
+        enabled: false,
+    },
+
+    required: ["allowed_privapp_com.android.car.settings"],
+
+    dxflags: ["--multi-dex"],
+
+    product_variables: {
+        pdk: {
+            enabled: false,
+        },
+    },
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/AndroidManifest.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/AndroidManifest.xml
new file mode 100644
index 0000000..82fcdf5
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/AndroidManifest.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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.settings"
+          android:sharedUserId="android.uid.system"
+          coreApp="true">
+</manifest>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/settings_recyclerview_default.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/settings_recyclerview_default.xml
new file mode 100644
index 0000000..4616fdf
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/settings_recyclerview_default.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <com.android.car.ui.FocusArea
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        android:id="@+id/settings_car_ui_focus_area"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <com.android.car.ui.recyclerview.CarUiRecyclerView
+            android:id="@+id/settings_recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:tag="carUiPreferenceRecyclerView"
+            app:carUiSize="small"
+            app:enableDivider="true" />
+    </com.android.car.ui.FocusArea>
+</merge>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/top_level_recyclerview.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/top_level_recyclerview.xml
new file mode 100644
index 0000000..4dbe9be
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/top_level_recyclerview.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <com.android.car.ui.FocusArea
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        android:id="@+id/settings_car_ui_focus_area"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <com.android.car.ui.recyclerview.CarUiRecyclerView
+            android:id="@+id/top_level_recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:tag="carUiPreferenceRecyclerView"
+            app:carUiSize="small"
+            app:enableDivider="true" />
+    </com.android.car.ui.FocusArea>
+</merge>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/config.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/config.xml
new file mode 100644
index 0000000..eaf603f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/config.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.
+  -->
+
+<resources>
+    <bool name="config_global_force_single_pane">false</bool>
+    <string name="config_homepage_fragment_class" translatable="false">com.android.car.settings.bluetooth.BluetoothSettingsFragment</string>
+    <bool name="config_top_level_enable_chevrons">false</bool>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/dimens.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/dimens.xml
new file mode 100644
index 0000000..f2a58a4
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2018 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>
+    <!-- Top-level menu -->
+    <dimen name="top_level_menu_width">400dp</dimen>
+    <dimen name="top_level_recyclerview_margin_right">@*android:dimen/car_padding_2</dimen>
+    <dimen name="top_level_foreground_icon_inset">8dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp
new file mode 100644
index 0000000..01717c7
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp
@@ -0,0 +1,77 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "CarUiPortraitSystemUI",
+
+    srcs: ["src/**/*.java"],
+
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "CarSystemUI-core",
+    ],
+
+    libs: [
+        "android.car",
+    ],
+
+    manifest: "AndroidManifest.xml",
+
+    overrides: [
+        "CarSystemUI",
+    ],
+
+    platform_apis: true,
+    system_ext_specific: true,
+    certificate: "platform",
+    privileged: true,
+
+    optimize: {
+        proguard_flags_files: [
+            "proguard.flags",
+        ],
+    },
+    dxflags: ["--multi-dex"],
+
+    plugins: ["dagger2-compiler"],
+
+    required: ["privapp_whitelist_com.android.systemui", "allowed_privapp_com.android.carsystemui"],
+}
+
+//####################################################################################
+// Build a static library to help mocking in testing. This is meant to be used
+// for internal unit tests.
+//####################################################################################
+android_library {
+    name: "CarUiPortraitSystemUI-tests",
+
+    srcs: ["src/**/*.java"],
+
+    resource_dirs: ["res"],
+
+    libs: [
+        "android.car",
+    ],
+
+    static_libs: [
+        "CarSystemUI-tests",
+    ],
+
+    plugins: ["dagger2-compiler"],
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/AndroidManifest.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/AndroidManifest.xml
new file mode 100644
index 0000000..fc2e241
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/AndroidManifest.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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+          package="com.android.systemui"
+          android:sharedUserId="android.uid.systemui"
+          coreApp="true">
+</manifest>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/proguard.flags b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/proguard.flags
new file mode 100644
index 0000000..3de0064
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/proguard.flags
@@ -0,0 +1,6 @@
+-keep class com.android.systemui.CarUiPortraitSystemUIFactory
+
+-keep class com.android.systemui.DaggerCarUiPortraitGlobalRootComponent { *; }
+-keep class com.android.systemui.DaggerCarUiPortraitGlobalRootComponent$CarUiPortraitSysUIComponentImpl { *; }
+
+-include ../../../../../../../packages/apps/Car/SystemUI/proguard.flags
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_apps.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_apps.xml
new file mode 100644
index 0000000..a98b3a7
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_apps.xml
@@ -0,0 +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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <vector android:width="@dimen/system_bar_icon_drawing_size"
+                android:height="@dimen/system_bar_icon_drawing_size"
+                android:viewportWidth="24.0"
+                android:viewportHeight="24.0">
+            <path
+                android:fillColor="@color/car_nav_icon_fill_color"
+                android:pathData="M6,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM6,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM6,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM16,6c0,1.1 0.9,2 2,2s2,-0.9 2,-2 -0.9,-2 -2,-2 -2,0.9 -2,2zM12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z"/>
+        </vector>
+    </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_hvac.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_hvac.xml
new file mode 100644
index 0000000..b42c86c
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_hvac.xml
@@ -0,0 +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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <vector android:width="@dimen/system_bar_icon_drawing_size"
+                android:height="@dimen/system_bar_icon_drawing_size"
+                android:viewportWidth="24"
+                android:viewportHeight="24">
+            <path
+                android:fillColor="@color/car_nav_icon_fill_color"
+                android:pathData="M16.34,8.36l-2.29,0.82c-0.18,-0.13 -0.38,-0.25 -0.58,-0.34c0.17,-0.83 0.63,-1.58 1.36,-2.06C16.85,5.44 16.18,2 13.39,2C9,2 7.16,5.01 8.36,7.66l0.82,2.29c-0.13,0.18 -0.25,0.38 -0.34,0.58c-0.83,-0.17 -1.58,-0.63 -2.06,-1.36C5.44,7.15 2,7.82 2,10.61c0,4.4 3.01,6.24 5.66,5.03l2.29,-0.82c0.18,0.13 0.38,0.25 0.58,0.34c-0.17,0.83 -0.63,1.58 -1.36,2.06C7.15,18.56 7.82,22 10.61,22c4.4,0 6.24,-3.01 5.03,-5.66l-0.82,-2.29c0.13,-0.18 0.25,-0.38 0.34,-0.58c0.83,0.17 1.58,0.63 2.06,1.36c1.34,2.01 4.77,1.34 4.77,-1.45C22,9 18.99,7.16 16.34,8.36zM12,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5c0.83,0 1.5,0.67 1.5,1.5C13.5,12.83 12.83,13.5 12,13.5zM10.24,5.22C10.74,4.44 11.89,4 13.39,4c0.79,0 0.71,0.86 0.34,1.11c-1.22,0.81 -2,2.06 -2.25,3.44c-0.21,0.03 -0.42,0.08 -0.62,0.15l-0.68,-1.88C10,6.42 9.86,5.81 10.24,5.22zM6.83,13.82c-0.4,0.18 -1.01,0.32 -1.61,-0.06C4.44,13.26 4,12.11 4,10.61c0,-0.79 0.86,-0.71 1.11,-0.34c0.81,1.22 2.06,2 3.44,2.25c0.03,0.21 0.08,0.42 0.15,0.62L6.83,13.82zM13.76,18.78c-0.5,0.77 -1.65,1.22 -3.15,1.22c-0.79,0 -0.71,-0.86 -0.34,-1.11c1.22,-0.81 2,-2.06 2.25,-3.44c0.21,-0.03 0.42,-0.08 0.62,-0.15l0.68,1.88C14,17.58 14.14,18.18 13.76,18.78zM18.89,13.73c-0.81,-1.22 -2.06,-2 -3.44,-2.25c-0.03,-0.21 -0.08,-0.42 -0.15,-0.62l1.88,-0.68c0.4,-0.18 1.01,-0.32 1.61,0.06c0.77,0.5 1.22,1.65 1.22,3.15C20,14.19 19.14,14.11 18.89,13.73z"/>
+        </vector>
+    </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_mic.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_mic.xml
new file mode 100644
index 0000000..f282b65
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_mic.xml
@@ -0,0 +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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <vector android:width="@dimen/system_bar_icon_drawing_size"
+                android:height="@dimen/system_bar_icon_drawing_size"
+                android:viewportWidth="24.0"
+                android:viewportHeight="24.0">
+            <path
+                android:fillColor="@color/car_nav_icon_fill_color"
+                android:pathData="M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM11,5c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v6c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L11,5zM17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5L5,11c0,3.53 2.61,6.43 6,6.92L11,21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92h-2z"/>
+        </vector>
+    </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_notification.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_notification.xml
new file mode 100644
index 0000000..27b69a8
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_notification.xml
@@ -0,0 +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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <vector android:width="@dimen/system_bar_icon_drawing_size"
+                android:height="@dimen/system_bar_icon_drawing_size"
+                android:viewportWidth="24.0"
+                android:viewportHeight="24.0">
+            <path
+                android:fillColor="@color/car_nav_icon_fill_color"
+                android:pathData="M18,17v-6c0,-3.07 -1.63,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.64,5.36 6,7.92 6,11v6L4,17v2h16v-2h-2zM16,17L8,17v-6c0,-2.48 1.51,-4.5 4,-4.5s4,2.02 4,4.5v6zM12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2z"/>
+        </vector>
+    </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_user_icon.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_user_icon.xml
new file mode 100644
index 0000000..45887dc
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_user_icon.xml
@@ -0,0 +1,26 @@
+<?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/system_bar_user_icon_drawing_size"
+    android:height="@dimen/system_bar_user_icon_drawing_size"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="@color/system_bar_icon_color"
+      android:pathData="M12,5.9c1.16,0 2.1,0.94 2.1,2.1s-0.94,2.1 -2.1,2.1S9.9,9.16 9.9,8s0.94,-2.1 2.1,-2.1m0,9c2.97,0 6.1,1.46 6.1,2.1v1.1L5.9,18.1L5.9,17c0,-0.64 3.13,-2.1 6.1,-2.1M12,4C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,13c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-2.66 -5.33,-4 -8,-4z"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar.xml
new file mode 100644
index 0000000..7e72373
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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:id="@android:id/background">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/hvac_off_background_color" />
+        </shape>
+    </item>
+    <item android:id="@android:id/progress">
+        <clip>
+            <shape android:shape="rectangle">
+                <solid android:color="@color/hvac_on_background_color" />
+            </shape>
+        </clip>
+    </item>
+</layer-list>
\ No newline at end of file
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
new file mode 100644
index 0000000..7104440
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_background.xml
@@ -0,0 +1,42 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/hvac_panel_seek_bar_radius"/>
+            <solid android:color="@color/hvac_off_background_color" />
+        </shape>
+    </item>
+    <item
+        android:gravity="left"
+        android:width="@dimen/hvac_panel_button_dimen">
+        <selector>
+            <item android:state_selected="true">
+                <shape android:shape="rectangle">
+                    <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_seek_bar_radius"/>
+                    <solid android:color="@color/hvac_off_background_color" />
+                </shape>
+            </item>
+        </selector>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb.xml
new file mode 100644
index 0000000..63b731f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb.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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="oval">
+            <solid android:color="@color/hvac_on_background_color"/>
+            <size android:height="@dimen/hvac_panel_button_dimen"
+                  android:width="@dimen/hvac_panel_button_dimen"/>
+        </shape>
+    </item>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_1.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_1.xml
new file mode 100644
index 0000000..a0befd8
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_1.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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_on_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_1"
+        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/fan_speed_seek_bar_thumb_2.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_2.xml
new file mode 100644
index 0000000..c0725c3
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_2.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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_on_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_2"
+        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/fan_speed_seek_bar_thumb_3.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_3.xml
new file mode 100644
index 0000000..d11d90b
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_3.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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_on_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_3"
+        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/fan_speed_seek_bar_thumb_4.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_4.xml
new file mode 100644
index 0000000..177d9a4
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_4.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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_on_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_4"
+        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/fan_speed_seek_bar_thumb_5.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_5.xml
new file mode 100644
index 0000000..c87f92a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_5.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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_on_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_5"
+        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/fan_speed_seek_bar_thumb_6.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_6.xml
new file mode 100644
index 0000000..fc8452d
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_6.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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_on_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_6"
+        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/fan_speed_seek_bar_thumb_7.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_7.xml
new file mode 100644
index 0000000..4531e65
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_7.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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_on_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_7"
+        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/fan_speed_seek_bar_thumb_8.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_8.xml
new file mode 100644
index 0000000..9905a24
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_8.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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_on_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_8"
+        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_cool_on_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_cool_on_bg.xml
new file mode 100644
index 0000000..711158c
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_cool_on_bg.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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <solid android:color="@color/hvac_on_cooling_background_color"/>
+            <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+        </shape>
+    </item>
+    <item>
+        <ripple android:color="@color/car_ui_ripple_color">
+            <item android:id="@android:id/mask">
+                <shape>
+                    <solid android:color="?android:colorAccent"/>
+                    <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+                </shape>
+            </item>
+        </ripple>
+    </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_heat_on_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_heat_on_bg.xml
new file mode 100644
index 0000000..d069bd9
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_heat_on_bg.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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <solid android:color="@color/hvac_on_heating_background_color"/>
+            <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+        </shape>
+    </item>
+    <item>
+        <ripple android:color="@color/car_ui_ripple_color">
+            <item android:id="@android:id/mask">
+                <shape>
+                    <solid android:color="?android:colorAccent"/>
+                    <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+                </shape>
+            </item>
+        </ripple>
+    </item>
+</layer-list>
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
new file mode 100644
index 0000000..d40ad01
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_off_bg.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>
+        <shape>
+            <solid android:color="@color/hvac_off_background_color"/>
+            <corners android:radius="@dimen/hvac_panel_off_button_radius"/>
+        </shape>
+    </item>
+    <item>
+        <ripple android:color="@color/car_ui_ripple_color">
+            <item android:id="@android:id/mask">
+                <shape>
+                    <solid android:color="?android:colorAccent"/>
+                    <corners android:radius="@dimen/hvac_panel_off_button_radius"/>
+                </shape>
+            </item>
+        </ripple>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_on_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_on_bg.xml
new file mode 100644
index 0000000..a5d66bc
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_on_bg.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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <solid android:color="@color/hvac_on_background_color"/>
+            <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+        </shape>
+    </item>
+    <item>
+        <ripple android:color="@color/car_ui_ripple_color">
+            <item android:id="@android:id/mask">
+                <shape>
+                    <solid android:color="?android:colorAccent"/>
+                    <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+                </shape>
+            </item>
+        </ripple>
+    </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_cool_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_cool_background.xml
new file mode 100644
index 0000000..b1f9e79
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_cool_background.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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"
+          android:drawable="@drawable/hvac_button_cool_on_bg"/>
+    <item android:drawable="@drawable/hvac_button_off_bg"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_decrease_button.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_decrease_button.xml
new file mode 100644
index 0000000..ea3a853
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_decrease_button.xml
@@ -0,0 +1,50 @@
+<?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"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <item>
+        <aapt:attr name="android:drawable">
+            <vector android:width="@dimen/hvac_temperature_button_size"
+                    android:height="@dimen/hvac_temperature_button_size"
+                    android:viewportWidth="64"
+                    android:viewportHeight="64">
+                <path
+                    android:pathData="M32,0L32,0A32,32 0,0 1,64 32L64,32A32,32 0,0 1,32 64L32,64A32,32 0,0 1,0 32L0,32A32,32 0,0 1,32 0z"
+                    android:fillColor="@color/hvac_temperature_adjust_button_color"/>
+            </vector>
+        </aapt:attr>
+    </item>
+    <item android:gravity="center">
+        <aapt:attr name="android:drawable">
+            <vector android:width="24dp"
+                    android:height="3dp"
+                    android:viewportWidth="24"
+                    android:viewportHeight="3">
+                <path
+                    android:fillColor="@color/hvac_temperature_decrease_arrow_color"
+                    android:pathData="M24,3.5H0V0.5H24V3.5Z"/>
+            </vector>
+        </aapt:attr>
+    </item>
+    <item>
+        <aapt:attr name="android:drawable">
+            <ripple android:color="?android:attr/colorControlHighlight"/>
+        </aapt:attr>
+    </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_default_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_default_background.xml
new file mode 100644
index 0000000..84f502b
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_default_background.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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"
+          android:drawable="@drawable/hvac_button_on_bg"/>
+    <item android:drawable="@drawable/hvac_button_off_bg"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_heat_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_heat_background.xml
new file mode 100644
index 0000000..09d091e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_heat_background.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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"
+          android:drawable="@drawable/hvac_button_heat_on_bg"/>
+    <item android:drawable="@drawable/hvac_button_off_bg"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_increase_button.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_increase_button.xml
new file mode 100644
index 0000000..630727d
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_increase_button.xml
@@ -0,0 +1,53 @@
+<?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"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <item>
+        <aapt:attr name="android:drawable">
+            <vector android:width="@dimen/hvac_temperature_button_size"
+                    android:height="@dimen/hvac_temperature_button_size"
+                    android:viewportWidth="64"
+                    android:viewportHeight="64">
+                <path
+                    android:pathData="M32,0L32,0A32,32 0,0 1,64 32L64,32A32,32 0,0 1,32 64L32,64A32,32 0,0 1,0 32L0,32A32,32 0,0 1,32 0z"
+                    android:fillColor="@color/hvac_temperature_adjust_button_color"/>
+            </vector>
+        </aapt:attr>
+    </item>
+    <item
+        android:gravity="center"
+        android:width="24dp"
+        android:height="24dp">
+        <aapt:attr name="android:drawable">
+            <vector android:width="24dp"
+                    android:height="24dp"
+                    android:viewportWidth="24"
+                    android:viewportHeight="24">
+                <path
+                    android:fillColor="@color/hvac_temperature_increase_arrow_color"
+                    android:pathData="M24,13.5H13.5V24H10.5V13.5H0V10.5H10.5V0H13.5V10.5H24V13.5Z"/>
+            </vector>
+        </aapt:attr>
+    </item>
+    <item>
+        <aapt:attr name="android:drawable">
+            <ripple android:color="?android:attr/colorControlHighlight"/>
+        </aapt:attr>
+    </item>
+</layer-list>
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
new file mode 100644
index 0000000..f0cd3bd
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_panel_bg.xml
@@ -0,0 +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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/hvac_background_color"/>
+
+    <!-- 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_ac_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_off.xml
new file mode 100644
index 0000000..5458c73
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_off.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="M42.0001,22H35.6601L40.7401,16.92C41.5201,16.14 41.5201,14.88 40.7401,14.1C39.9601,13.32 38.6801,13.32 37.9001,14.1L30.0001,22H26.0001V18L33.9001,10.1C34.6801,9.32 34.6801,8.04 33.9001,7.26C33.1201,6.48 31.8601,6.48 31.0801,7.26L26.0001,12.34V6C26.0001,4.9 25.1001,4 24.0001,4C22.9001,4 22.0001,4.9 22.0001,6V12.34L16.9201,7.26C16.1401,6.48 14.8801,6.48 14.1001,7.26C13.3201,8.04 13.3201,9.32 14.1001,10.1L22.0001,18V20.34L27.6601,26H30.0001L37.9001,33.9C38.6801,34.68 39.9601,34.68 40.7401,33.9C41.5201,33.12 41.5201,31.86 40.7401,31.08L35.6601,26H42.0001C43.1001,26 44.0001,25.1 44.0001,24C44.0001,22.9 43.1001,22 42.0001,22Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M1.5801,11.24L12.3401,22H6.0001C4.9001,22 4.0001,22.9 4.0001,24C4.0001,25.1 4.9001,26 6.0001,26H12.3401L7.2601,31.08C6.4801,31.86 6.4801,33.12 7.2601,33.9C8.0401,34.68 9.3201,34.68 10.1001,33.9L17.1801,26.82L21.1801,30.82L14.1001,37.9C13.3201,38.68 13.3201,39.96 14.1001,40.74C14.8801,41.52 16.1401,41.52 16.9201,40.74L22.0001,35.66V42C22.0001,43.1 22.9001,44 24.0001,44C25.1001,44 26.0001,43.1 26.0001,42V35.66L38.3401,48L41.1601,45.18L4.4001,8.4L1.5801,11.24Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_on.xml
new file mode 100644
index 0000000..f86563e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_on.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="M42,22H35.66L40.74,16.92C41.52,16.14 41.52,14.88 40.74,14.1C39.96,13.32 38.68,13.32 37.9,14.1L30,22H26V18L33.9,10.1C34.68,9.32 34.68,8.04 33.9,7.26C33.12,6.48 31.86,6.48 31.08,7.26L26,12.34V6C26,4.9 25.1,4 24,4C22.9,4 22,4.9 22,6V12.34L16.92,7.26C16.14,6.48 14.88,6.48 14.1,7.26C13.32,8.04 13.32,9.32 14.1,10.1L22,18V22H18L10.1,14.1C9.32,13.32 8.04,13.32 7.26,14.1C6.48,14.88 6.48,16.14 7.26,16.92L12.34,22H6C4.9,22 4,22.9 4,24C4,25.1 4.9,26 6,26H12.34L7.26,31.08C6.48,31.86 6.48,33.12 7.26,33.9C8.04,34.68 9.32,34.68 10.1,33.9L18,26H22V30L14.1,37.9C13.32,38.68 13.32,39.96 14.1,40.74C14.88,41.52 16.14,41.52 16.92,40.74L22,35.66V42C22,43.1 22.9,44 24,44C25.1,44 26,43.1 26,42V35.66L31.08,40.74C31.86,41.52 33.12,41.52 33.9,40.74C34.68,39.96 34.68,38.68 33.9,37.9L26,30V26H30L37.9,33.9C38.68,34.68 39.96,34.68 40.74,33.9C41.52,33.12 41.52,31.86 40.74,31.08L35.66,26H42C43.1,26 44,25.1 44,24C44,22.9 43.1,22 42,22Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M42,22H35.66L40.74,16.92C41.52,16.14 41.52,14.88 40.74,14.1C39.96,13.32 38.68,13.32 37.9,14.1L30,22H26V18L33.9,10.1C34.68,9.32 34.68,8.04 33.9,7.26C33.12,6.48 31.86,6.48 31.08,7.26L26,12.34V6C26,4.9 25.1,4 24,4C22.9,4 22,4.9 22,6V12.34L16.92,7.26C16.14,6.48 14.88,6.48 14.1,7.26C13.32,8.04 13.32,9.32 14.1,10.1L22,18V22H18L10.1,14.1C9.32,13.32 8.04,13.32 7.26,14.1C6.48,14.88 6.48,16.14 7.26,16.92L12.34,22H6C4.9,22 4,22.9 4,24C4,25.1 4.9,26 6,26H12.34L7.26,31.08C6.48,31.86 6.48,33.12 7.26,33.9C8.04,34.68 9.32,34.68 10.1,33.9L18,26H22V30L14.1,37.9C13.32,38.68 13.32,39.96 14.1,40.74C14.88,41.52 16.14,41.52 16.92,40.74L22,35.66V42C22,43.1 22.9,44 24,44C25.1,44 26,43.1 26,42V35.66L31.08,40.74C31.86,41.52 33.12,41.52 33.9,40.74C34.68,39.96 34.68,38.68 33.9,37.9L26,30V26H30L37.9,33.9C38.68,34.68 39.96,34.68 40.74,33.9C41.52,33.12 41.52,31.86 40.74,31.08L35.66,26H42C43.1,26 44,25.1 44,24C44,22.9 43.1,22 42,22Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_off.xml
new file mode 100644
index 0000000..3cf394e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_off.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="49">
+  <path
+      android:pathData="M40.209,16.7912L38.789,15.3812L36.999,17.1712L36.999,3.0012L34.999,3.0012L34.999,17.1712L33.209,15.3812L31.789,16.7912L35.999,21.0012L40.209,16.7912Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M34.0338,32.7879L24.8823,46.6596C24.5032,47.2343 24.9153,48 25.6038,48C27.1419,48 28.6223,47.4142 29.744,46.3617L39.1124,37.5709C40.9662,35.8313 43.413,34.8632 45.9551,34.8632H54.2319C56.5346,34.8632 58.6342,33.5453 59.6353,31.4715L66.7164,16.8024C67.3096,15.5736 66.4143,14.1474 65.0499,14.1474C64.0736,14.1474 63.155,14.6096 62.5732,15.3935L55.9967,24.2545C54.1102,26.7962 51.1319,28.2947 47.9666,28.2947H42.3809C39.0203,28.2947 35.8844,29.9828 34.0338,32.7879Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M61.9985,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_on.xml
new file mode 100644
index 0000000..a4c1eb2
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_on.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="49">
+  <path
+      android:pathData="M40.209,16.7912L38.789,15.3812L36.999,17.1712L36.999,3.0012L34.999,3.0012L34.999,17.1712L33.209,15.3812L31.789,16.7912L35.999,21.0012L40.209,16.7912Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M34.0338,32.7879L24.8823,46.6596C24.5032,47.2343 24.9153,48 25.6038,48C27.1419,48 28.6223,47.4142 29.744,46.3617L39.1124,37.5709C40.9662,35.8313 43.413,34.8632 45.9551,34.8632H54.2319C56.5346,34.8632 58.6342,33.5453 59.6353,31.4715L66.7164,16.8024C67.3096,15.5736 66.4143,14.1474 65.0499,14.1474C64.0736,14.1474 63.155,14.6096 62.5732,15.3935L55.9967,24.2545C54.1102,26.7962 51.1319,28.2947 47.9666,28.2947H42.3809C39.0203,28.2947 35.8844,29.9828 34.0338,32.7879Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M61.9985,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_off.xml
new file mode 100644
index 0000000..981958a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_off.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M40.79,7.79L39.38,9.21L41.17,11H27V13H41.17L39.38,14.79L40.79,16.21L45,12L40.79,7.79Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M34.0357,32.7879L24.8843,46.6596C24.5051,47.2343 24.9173,48 25.6058,48C27.1439,48 28.6243,47.4142 29.746,46.3617L39.1144,37.5709C40.9682,35.8313 43.4149,34.8632 45.9571,34.8632H54.2339C56.5366,34.8632 58.6362,33.5453 59.6372,31.4715L66.7184,16.8024C67.3115,15.5736 66.4162,14.1474 65.0518,14.1474C64.0756,14.1474 63.157,14.6096 62.5752,15.3935L55.9986,24.2545C54.1122,26.7962 51.1338,28.2947 47.9686,28.2947H42.3829C39.0223,28.2947 35.8864,29.9828 34.0357,32.7879Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M62.0005,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_on.xml
new file mode 100644
index 0000000..4dc3272
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_on.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M40.79,7.79L39.38,9.21L41.17,11H27V13H41.17L39.38,14.79L40.79,16.21L45,12L40.79,7.79Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M34.0357,32.7879L24.8843,46.6596C24.5051,47.2343 24.9173,48 25.6058,48C27.1439,48 28.6243,47.4142 29.746,46.3617L39.1144,37.5709C40.9682,35.8313 43.4149,34.8632 45.9571,34.8632H54.2339C56.5366,34.8632 58.6362,33.5453 59.6372,31.4715L66.7184,16.8024C67.3115,15.5736 66.4162,14.1474 65.0518,14.1474C64.0756,14.1474 63.157,14.6096 62.5752,15.3935L55.9986,24.2545C54.1122,26.7962 51.1338,28.2947 47.9686,28.2947H42.3829C39.0223,28.2947 35.8864,29.9828 34.0357,32.7879Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M62.0005,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_off.xml
new file mode 100644
index 0000000..2cdfa90
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_off.xml
@@ -0,0 +1,53 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="49">
+  <path
+      android:pathData="M34.0338,32.7879L24.8823,46.6596C24.5032,47.2343 24.9153,48 25.6038,48C27.1419,48 28.6223,47.4142 29.744,46.3617L39.1124,37.5709C40.9662,35.8313 43.413,34.8632 45.9551,34.8632H54.2319C56.5346,34.8632 58.6342,33.5453 59.6353,31.4715L66.7164,16.8024C67.3096,15.5736 66.4143,14.1474 65.0499,14.1474C64.0736,14.1474 63.155,14.6096 62.5732,15.3935L55.9967,24.2545C54.1102,26.7962 51.1319,28.2947 47.9666,28.2947H42.3809C39.0203,28.2947 35.8844,29.9828 34.0338,32.7879Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M61.9985,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M41.5603,10.0488C37.5686,12.8863 45.4033,16.6289 41.5603,19.0972"
+      android:strokeWidth="2"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M37.0374,10.0488C33.0456,12.8863 40.8804,16.6289 37.0374,19.0972"
+      android:strokeWidth="2"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M32.5135,10.0488C28.5217,12.8863 36.3565,16.629 32.5135,19.0973"
+      android:strokeWidth="2"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M28.8859,15.7037H27.804L25.0881,6.2391C24.7768,5.1544 25.3126,4.0062 26.3439,3.5479C30.1667,1.8493 33.7188,1 37,1C40.2812,1 43.8333,1.8493 47.6561,3.5479C48.6874,4.0061 49.2232,5.1544 48.9119,6.2391L46.196,15.7037H45.1141"
+      android:strokeLineJoin="round"
+      android:strokeWidth="2"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_on.xml
new file mode 100644
index 0000000..a3510b1
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_on.xml
@@ -0,0 +1,53 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="49">
+  <path
+      android:pathData="M34.0338,32.7879L24.8823,46.6596C24.5032,47.2343 24.9153,48 25.6038,48C27.1419,48 28.6223,47.4142 29.744,46.3617L39.1124,37.5709C40.9662,35.8313 43.413,34.8632 45.9551,34.8632H54.2319C56.5346,34.8632 58.6342,33.5453 59.6353,31.4715L66.7164,16.8024C67.3096,15.5736 66.4143,14.1474 65.0499,14.1474C64.0736,14.1474 63.155,14.6096 62.5732,15.3935L55.9967,24.2545C54.1102,26.7962 51.1319,28.2947 47.9666,28.2947H42.3809C39.0203,28.2947 35.8844,29.9828 34.0338,32.7879Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M61.9985,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M41.5603,10.0488C37.5686,12.8863 45.4033,16.6289 41.5603,19.0972"
+      android:strokeWidth="2"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M37.0374,10.0488C33.0456,12.8863 40.8804,16.6289 37.0374,19.0972"
+      android:strokeWidth="2"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M32.5135,10.0488C28.5217,12.8863 36.3565,16.629 32.5135,19.0973"
+      android:strokeWidth="2"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M28.8859,15.7037H27.804L25.0881,6.2391C24.7768,5.1544 25.3126,4.0062 26.3439,3.5479C30.1667,1.8493 33.7188,1 37,1C40.2812,1 43.8333,1.8493 47.6561,3.5479C48.6874,4.0061 49.2232,5.1544 48.9119,6.2391L46.196,15.7037H45.1141"
+      android:strokeLineJoin="round"
+      android:strokeWidth="2"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_off.xml
new file mode 100644
index 0000000..65a4b12
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_off.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.
+  -->
+
+<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="M0.2653,30L4.6853,17.828H7.3203L11.7573,30H9.2243L8.2383,27.093H3.7843L2.7983,30H0.2653ZM5.5183,21.942L4.4983,24.985H7.5243L6.5043,21.942L6.0793,20.48H5.9433L5.5183,21.942ZM17.9681,30.272C16.4721,30.272 15.2934,29.8243 14.4321,28.929C13.5821,28.0337 13.1571,26.7927 13.1571,25.206V17.828H15.4181V25.359C15.4181,26.2203 15.6334,26.8947 16.0641,27.382C16.4947,27.858 17.1294,28.096 17.9681,28.096C18.8067,28.096 19.4357,27.858 19.8551,27.382C20.2857,26.8947 20.5011,26.2203 20.5011,25.359V17.828H22.7621V25.206C22.7621,26.226 22.5694,27.1157 22.1841,27.875C21.7987,28.6343 21.2491,29.2237 20.5351,29.643C19.8211,30.0623 18.9654,30.272 17.9681,30.272ZM28.299,30V20.004H24.559V17.828H34.317V20.004H30.577V30H28.299ZM41.1274,30.272C39.926,30.272 38.8834,30 37.9994,29.456C37.1154,28.9007 36.4297,28.147 35.9424,27.195C35.455,26.2317 35.2114,25.138 35.2114,23.914C35.2114,22.6787 35.455,21.585 35.9424,20.633C36.4297,19.681 37.1154,18.933 37.9994,18.389C38.8834,17.8337 39.926,17.556 41.1274,17.556C42.3287,17.556 43.3714,17.8337 44.2554,18.389C45.1394,18.933 45.825,19.681 46.3124,20.633C46.7997,21.585 47.0434,22.6787 47.0434,23.914C47.0434,25.138 46.7997,26.2317 46.3124,27.195C45.825,28.147 45.1394,28.9007 44.2554,29.456C43.3714,30 42.3287,30.272 41.1274,30.272ZM41.1274,28.096C42.204,28.096 43.071,27.7333 43.7284,27.008C44.397,26.2827 44.7314,25.2513 44.7314,23.914C44.7314,22.5767 44.397,21.5453 43.7284,20.82C43.071,20.0947 42.204,19.732 41.1274,19.732C40.0507,19.732 39.178,20.0947 38.5094,20.82C37.852,21.5453 37.5234,22.5767 37.5234,23.914C37.5234,25.2513 37.852,26.2827 38.5094,27.008C39.178,27.7333 40.0507,28.096 41.1274,28.096Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_on.xml
new file mode 100644
index 0000000..c847a95
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_on.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.
+  -->
+
+<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="M0.2653,30L4.6853,17.828H7.3203L11.7573,30H9.2243L8.2383,27.093H3.7843L2.7983,30H0.2653ZM5.5183,21.942L4.4983,24.985H7.5243L6.5043,21.942L6.0793,20.48H5.9433L5.5183,21.942ZM17.9681,30.272C16.4721,30.272 15.2934,29.8243 14.4321,28.929C13.5821,28.0337 13.1571,26.7927 13.1571,25.206V17.828H15.4181V25.359C15.4181,26.2203 15.6334,26.8947 16.0641,27.382C16.4947,27.858 17.1294,28.096 17.9681,28.096C18.8067,28.096 19.4357,27.858 19.8551,27.382C20.2857,26.8947 20.5011,26.2203 20.5011,25.359V17.828H22.7621V25.206C22.7621,26.226 22.5694,27.1157 22.1841,27.875C21.7987,28.6343 21.2491,29.2237 20.5351,29.643C19.8211,30.0623 18.9654,30.272 17.9681,30.272ZM28.299,30V20.004H24.559V17.828H34.317V20.004H30.577V30H28.299ZM41.1274,30.272C39.926,30.272 38.8834,30 37.9994,29.456C37.1154,28.9007 36.4297,28.147 35.9424,27.195C35.455,26.2317 35.2114,25.138 35.2114,23.914C35.2114,22.6787 35.455,21.585 35.9424,20.633C36.4297,19.681 37.1154,18.933 37.9994,18.389C38.8834,17.8337 39.926,17.556 41.1274,17.556C42.3287,17.556 43.3714,17.8337 44.2554,18.389C45.1394,18.933 45.825,19.681 46.3124,20.633C46.7997,21.585 47.0434,22.6787 47.0434,23.914C47.0434,25.138 46.7997,26.2317 46.3124,27.195C45.825,28.147 45.1394,28.9007 44.2554,29.456C43.3714,30 42.3287,30.272 41.1274,30.272ZM41.1274,28.096C42.204,28.096 43.071,27.7333 43.7284,27.008C44.397,26.2827 44.7314,25.2513 44.7314,23.914C44.7314,22.5767 44.397,21.5453 43.7284,20.82C43.071,20.0947 42.204,19.732 41.1274,19.732C40.0507,19.732 39.178,20.0947 38.5094,20.82C37.852,21.5453 37.5234,22.5767 37.5234,23.914C37.5234,25.2513 37.852,26.2827 38.5094,27.008C39.178,27.7333 40.0507,28.096 41.1274,28.096Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_off.xml
new file mode 100644
index 0000000..29bae07
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_off.xml
@@ -0,0 +1,47 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M55.4836,23.5C49.4334,27.8006 61.3082,33.4732 55.4836,37.2143"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M48.6281,23.5C42.5779,27.8006 54.4527,33.4732 48.6281,37.2143"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M41.7707,23.5C35.7205,27.8006 47.5953,33.4732 41.7707,37.2143"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M36,32.5H32C30.8954,32.5 30,31.6046 30,30.5V13C30,11.8954 30.8954,11 32,11H64.5C65.6046,11 66.5,11.8954 66.5,13V30.5C66.5,31.6046 65.6046,32.5 64.5,32.5H62"
+      android:strokeLineJoin="round"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_on.xml
new file mode 100644
index 0000000..ca35897
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_on.xml
@@ -0,0 +1,47 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M55.4836,23.5C49.4334,27.8006 61.3082,33.4732 55.4836,37.2143"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M48.6281,23.5C42.5779,27.8006 54.4527,33.4732 48.6281,37.2143"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M41.7707,23.5C35.7205,27.8006 47.5953,33.4732 41.7707,37.2143"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M36,32.5H32C30.8954,32.5 30,31.6046 30,30.5V13C30,11.8954 30.8954,11 32,11H64.5C65.6046,11 66.5,11.8954 66.5,13V30.5C66.5,31.6046 65.6046,32.5 64.5,32.5H62"
+      android:strokeLineJoin="round"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_off.xml
new file mode 100644
index 0000000..99a55de
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_off.xml
@@ -0,0 +1,47 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M54.1007,23.7148C48.0506,28.0154 59.9254,33.6881 54.1007,37.4291"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M47.2453,23.7148C41.1951,28.0154 53.0699,33.6881 47.2453,37.4291"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M40.3878,23.7148C34.3377,28.0154 46.2125,33.6881 40.3878,37.4291"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M34.8897,32.2857H33.2499L29.1335,17.9407C28.6618,16.2966 29.4739,14.5563 31.0369,13.8618C36.831,11.2873 42.2146,10 47.1878,10C52.161,10 57.5447,11.2873 63.3387,13.8618C64.9018,14.5563 65.7139,16.2966 65.2421,17.9406L61.1257,32.2857H59.486"
+      android:strokeLineJoin="round"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_on.xml
new file mode 100644
index 0000000..9728826
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_on.xml
@@ -0,0 +1,47 @@
+<!--
+  ~ 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_wide_icon_dimen"
+    android:height="@dimen/hvac_panel_icon_dimen"
+    android:viewportWidth="96"
+    android:viewportHeight="48">
+  <path
+      android:pathData="M54.1007,23.7148C48.0506,28.0154 59.9254,33.6881 54.1007,37.4291"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M47.2453,23.7148C41.1951,28.0154 53.0699,33.6881 47.2453,37.4291"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M40.3878,23.7148C34.3377,28.0154 46.2125,33.6881 40.3878,37.4291"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M34.8897,32.2857H33.2499L29.1335,17.9407C28.6618,16.2966 29.4739,14.5563 31.0369,13.8618C36.831,11.2873 42.2146,10 47.1878,10C52.161,10 57.5447,11.2873 63.3387,13.8618C64.9018,14.5563 65.7139,16.2966 65.2421,17.9406L61.1257,32.2857H59.486"
+      android:strokeLineJoin="round"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+</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
new file mode 100644
index 0000000..650fd9e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_high.xml
@@ -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.
+  -->
+
+<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="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,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,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
new file mode 100644
index 0000000..a89ba0a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_low.xml
@@ -0,0 +1,33 @@
+<!--
+  ~ 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="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,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,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_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml
new file mode 100644
index 0000000..9117cac
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml
@@ -0,0 +1,34 @@
+<!--
+  ~ 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="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,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,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_fan_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_off.xml
new file mode 100644
index 0000000..649195b
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_off.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.
+  -->
+
+<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_fan_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_on.xml
new file mode 100644
index 0000000..62bb353
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_on.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.
+  -->
+
+<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_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_off.xml
new file mode 100644
index 0000000..48baaf5
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_off.xml
@@ -0,0 +1,49 @@
+<!--
+  ~ 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="M41.1457,5.5455C35.3706,9.6506 46.7056,15.0654 41.1457,18.6364"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M35.6907,5.5455C29.9155,9.6506 41.2505,15.0654 35.6907,18.6364"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M30.2356,5.5455C24.4604,9.6506 35.7955,15.0654 30.2356,18.6364"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M24,6.5455C12,6.5455 6.5454,16.9091 6.5454,24C6.5454,31.0909 12,41.4546 24,41.4546C36,41.4546 41.4545,32.5005 41.4545,25.0005"
+      android:strokeLineJoin="round"
+      android:strokeWidth="4"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M14.0005,24.0005L8.0005,24.5005L8.5005,28.0005L18.296,28.7261C20.3848,28.8809 22.0005,30.6207 22.0005,32.7152V41.0005H26.0005V32.7722C26.0005,30.6543 27.6514,28.9034 29.7656,28.7791L43.0005,28.0005L43.5005,24.5005L34.0005,24.0005L30.0005,22.0005H18.0005L14.0005,24.0005Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_on.xml
new file mode 100644
index 0000000..425f2f6
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_on.xml
@@ -0,0 +1,49 @@
+<!--
+  ~ 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="M41.1457,5.5455C35.3706,9.6506 46.7056,15.0654 41.1457,18.6364"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M35.6907,5.5455C29.9155,9.6506 41.2505,15.0654 35.6907,18.6364"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M30.2356,5.5455C24.4604,9.6506 35.7955,15.0654 30.2356,18.6364"
+      android:strokeWidth="3"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M24,6.5455C12,6.5455 6.5454,16.9091 6.5454,24C6.5454,31.0909 12,41.4546 24,41.4546C36,41.4546 41.4545,32.5005 41.4545,25.0005"
+      android:strokeLineJoin="round"
+      android:strokeWidth="4"
+      android:fillColor="@android:color/transparent"
+      android:strokeColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M14.0005,24.0005L8.0005,24.5005L8.5005,28.0005L18.296,28.7261C20.3848,28.8809 22.0005,30.6207 22.0005,32.7152V41.0005H26.0005V32.7722C26.0005,30.6543 27.6514,28.9034 29.7656,28.7791L43.0005,28.0005L43.5005,24.5005L34.0005,24.0005L30.0005,22.0005H18.0005L14.0005,24.0005Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_minimize.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_minimize.xml
new file mode 100644
index 0000000..721cec4
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_minimize.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <vector
+            android:width="@dimen/system_bar_icon_drawing_size"
+            android:height="@dimen/system_bar_icon_drawing_size"
+            android:viewportWidth="24"
+            android:viewportHeight="24">
+            <path
+                android:pathData="M12,15.375l-6,-6 1.4,-1.4 4.6,4.6 4.6,-4.6 1.4,1.4z"
+                android:fillColor="@color/car_nav_minimize_icon_fill_color"/>
+        </vector>
+    </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_1.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_1.xml
new file mode 100644
index 0000000..00a1ed8
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_1.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.
+  -->
+
+<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_on_icon_fill_color"/>
+  <path
+      android:pathData="M42.9713,47V38.792L41.3233,39.992L40.2673,38.376L43.4833,36.056H45.0673V47H42.9713Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_2.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_2.xml
new file mode 100644
index 0000000..3421517
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_2.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.
+  -->
+
+<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_on_icon_fill_color"/>
+  <path
+      android:pathData="M39.3867,47V45.096C39.3867,45.096 39.4987,44.984 39.7227,44.76C39.9467,44.536 40.2294,44.2533 40.5707,43.912C40.9227,43.5707 41.2854,43.2133 41.6587,42.84C42.032,42.4667 42.3734,42.12 42.6827,41.8C42.992,41.48 43.2214,41.24 43.3707,41.08C43.744,40.6747 44.0054,40.3333 44.1547,40.056C44.304,39.7787 44.3787,39.4587 44.3787,39.096C44.3787,38.744 44.2454,38.4347 43.9787,38.168C43.712,37.9013 43.3387,37.768 42.8587,37.768C42.3787,37.768 42.0054,37.9067 41.7387,38.184C41.472,38.4613 41.2854,38.7707 41.1787,39.112L39.2907,38.328C39.408,37.9333 39.616,37.544 39.9147,37.16C40.224,36.776 40.624,36.456 41.1147,36.2C41.616,35.9333 42.208,35.8 42.8907,35.8C43.6374,35.8 44.2774,35.944 44.8107,36.232C45.3547,36.52 45.7707,36.904 46.0587,37.384C46.3574,37.864 46.5067,38.4027 46.5067,39C46.5067,39.6827 46.3414,40.312 46.0107,40.888C45.68,41.464 45.2694,41.992 44.7787,42.472C44.6187,42.6213 44.5014,42.7333 44.4267,42.808C44.3627,42.872 44.2934,42.936 44.2187,43C44.1547,43.064 44.0534,43.1653 43.9147,43.304C43.7867,43.432 43.584,43.6347 43.3067,43.912C43.04,44.1893 42.6614,44.5733 42.1707,45.064L42.2187,45.16H46.6507V47H39.3867Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_3.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_3.xml
new file mode 100644
index 0000000..58f47fe
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_3.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.
+  -->
+
+<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_on_icon_fill_color"/>
+  <path
+      android:pathData="M42.9975,47.256C42.1122,47.256 41.3068,47.0213 40.5815,46.552C39.8562,46.072 39.3548,45.3467 39.0775,44.376L41.0615,43.592C41.3282,44.68 41.9735,45.224 42.9975,45.224C43.4455,45.224 43.8402,45.0907 44.1815,44.824C44.5228,44.5467 44.6935,44.1893 44.6935,43.752C44.6935,43.2827 44.5122,42.9147 44.1495,42.648C43.7868,42.3813 43.3068,42.248 42.7095,42.248H41.7655V40.344H42.6295C43.0668,40.344 43.4562,40.232 43.7975,40.008C44.1388,39.784 44.3095,39.4373 44.3095,38.968C44.3095,38.6053 44.1762,38.3067 43.9095,38.072C43.6535,37.8373 43.3122,37.72 42.8855,37.72C42.4162,37.72 42.0535,37.848 41.7975,38.104C41.5415,38.36 41.3655,38.6427 41.2695,38.952L39.3655,38.168C39.4935,37.8053 39.7068,37.4427 40.0055,37.08C40.3042,36.7173 40.6935,36.4133 41.1735,36.168C41.6535,35.9227 42.2295,35.8 42.9015,35.8C43.6055,35.8 44.2188,35.928 44.7415,36.184C45.2748,36.44 45.6908,36.792 45.9895,37.24C46.2882,37.6773 46.4375,38.1733 46.4375,38.728C46.4375,39.3573 46.2882,39.8747 45.9895,40.28C45.7015,40.6853 45.3868,40.9733 45.0455,41.144V41.272C45.5468,41.4747 45.9682,41.8 46.3095,42.248C46.6615,42.696 46.8375,43.2613 46.8375,43.944C46.8375,44.5733 46.6775,45.1387 46.3575,45.64C46.0375,46.1413 45.5895,46.536 45.0135,46.824C44.4375,47.112 43.7655,47.256 42.9975,47.256Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_4.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_4.xml
new file mode 100644
index 0000000..077d134
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_4.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.
+  -->
+
+<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_on_icon_fill_color"/>
+  <path
+      android:pathData="M38.7282,44.984V43.288L43.5922,36.056H45.8642V43.032H47.2242V44.984H45.8642V47H43.7682V44.984H38.7282ZM41.0482,43.032H43.7682V39.176H43.6402L41.0482,43.032Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_5.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_5.xml
new file mode 100644
index 0000000..74b7fc2
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_5.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.
+  -->
+
+<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_on_icon_fill_color"/>
+  <path
+      android:pathData="M42.9365,47.256C42.3925,47.256 41.8485,47.1493 41.3045,46.936C40.7712,46.7227 40.3018,46.3973 39.8965,45.96C39.4912,45.5227 39.2085,44.968 39.0485,44.296L40.9365,43.56C41.0538,44.0827 41.2832,44.5093 41.6245,44.84C41.9658,45.16 42.3978,45.32 42.9205,45.32C43.4218,45.32 43.8432,45.1493 44.1845,44.808C44.5365,44.4667 44.7125,44.0347 44.7125,43.512C44.7125,43 44.5472,42.5733 44.2165,42.232C43.8858,41.88 43.4538,41.704 42.9205,41.704C42.5898,41.704 42.2965,41.7733 42.0405,41.912C41.7845,42.0507 41.5658,42.2267 41.3845,42.44L39.3525,41.528L39.9765,36.056H46.1525V37.896H41.7205L41.3205,40.456L41.4485,40.488C41.6618,40.3067 41.9232,40.152 42.2325,40.024C42.5525,39.896 42.9258,39.832 43.3525,39.832C43.9605,39.832 44.5258,39.9867 45.0485,40.296C45.5712,40.6053 45.9925,41.0373 46.3125,41.592C46.6432,42.136 46.8085,42.776 46.8085,43.512C46.8085,44.2373 46.6432,44.8827 46.3125,45.448C45.9818,46.0133 45.5232,46.456 44.9365,46.776C44.3605,47.096 43.6938,47.256 42.9365,47.256Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_6.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_6.xml
new file mode 100644
index 0000000..503c42a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_6.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.
+  -->
+
+<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_on_icon_fill_color"/>
+  <path
+      android:pathData="M43.0392,47.256C42.4312,47.256 41.8765,47.1493 41.3752,46.936C40.8845,46.712 40.4685,46.4187 40.1272,46.056C39.4445,45.3413 39.1032,44.4667 39.1032,43.432C39.1032,42.696 39.2472,42.024 39.5352,41.416C39.8339,40.808 40.2072,40.1787 40.6552,39.528C41.0925,38.8987 41.5299,38.2693 41.9672,37.64C42.4152,37 42.8579,36.3653 43.2952,35.736L44.9272,36.856C44.5539,37.3787 44.1752,37.9067 43.7912,38.44C43.4179,38.9627 43.0445,39.4907 42.6712,40.024L42.7672,40.12C43.0125,40.0133 43.2899,39.96 43.5992,39.96C44.1539,39.96 44.6819,40.1093 45.1832,40.408C45.6845,40.7067 46.0952,41.1227 46.4152,41.656C46.7352,42.1893 46.8952,42.808 46.8952,43.512C46.8952,44.2373 46.7139,44.8827 46.3512,45.448C45.9885,46.0133 45.5139,46.456 44.9272,46.776C44.3405,47.096 43.7112,47.256 43.0392,47.256ZM42.9912,45.336C43.3219,45.336 43.6259,45.2613 43.9032,45.112C44.1805,44.952 44.4045,44.7387 44.5752,44.472C44.7565,44.1947 44.8472,43.88 44.8472,43.528C44.8472,43.1653 44.7565,42.8507 44.5752,42.584C44.4045,42.3067 44.1752,42.0933 43.8872,41.944C43.6099,41.784 43.3112,41.704 42.9912,41.704C42.6819,41.704 42.3832,41.784 42.0952,41.944C41.8179,42.0933 41.5885,42.3067 41.4072,42.584C41.2365,42.8507 41.1512,43.1653 41.1512,43.528C41.1512,43.88 41.2365,44.1947 41.4072,44.472C41.5885,44.7387 41.8179,44.952 42.0952,45.112C42.3725,45.2613 42.6712,45.336 42.9912,45.336Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_7.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_7.xml
new file mode 100644
index 0000000..bcf9158
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_7.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.
+  -->
+
+<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_on_icon_fill_color"/>
+  <path
+      android:pathData="M41.5709,47.256L39.8109,46.28L44.3709,38.12L44.3069,38.024H39.2829V36.056H46.7229V38.12C45.8695,39.6347 45.0109,41.1547 44.1469,42.68C43.2829,44.2053 42.4242,45.7307 41.5709,47.256Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_8.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_8.xml
new file mode 100644
index 0000000..7d70370
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_8.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.
+  -->
+
+<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_on_icon_fill_color"/>
+  <path
+      android:pathData="M43.0087,47.256C42.23,47.256 41.542,47.112 40.9447,46.824C40.3474,46.5253 39.878,46.1307 39.5367,45.64C39.206,45.1387 39.0407,44.5787 39.0407,43.96C39.0407,43.32 39.2114,42.776 39.5527,42.328C39.894,41.8693 40.278,41.5227 40.7047,41.288V41.16C40.3634,40.9253 40.0594,40.616 39.7927,40.232C39.5367,39.8373 39.4087,39.3893 39.4087,38.888C39.4087,38.3013 39.5634,37.7787 39.8727,37.32C40.182,36.8507 40.6087,36.4827 41.1527,36.216C41.6967,35.9387 42.3154,35.8 43.0087,35.8C43.702,35.8 44.3154,35.9387 44.8487,36.216C45.3927,36.4827 45.8194,36.8507 46.1287,37.32C46.4487,37.7787 46.6087,38.3013 46.6087,38.888C46.6087,39.3893 46.4754,39.8373 46.2087,40.232C45.9527,40.616 45.6487,40.9253 45.2967,41.16V41.288C45.734,41.5227 46.1234,41.8693 46.4647,42.328C46.806,42.776 46.9767,43.32 46.9767,43.96C46.9767,44.5787 46.806,45.1387 46.4647,45.64C46.134,46.1307 45.67,46.5253 45.0727,46.824C44.486,47.112 43.798,47.256 43.0087,47.256ZM43.0087,40.376C43.446,40.376 43.814,40.2533 44.1127,40.008C44.422,39.752 44.5767,39.4213 44.5767,39.016C44.5767,38.6 44.422,38.2747 44.1127,38.04C43.814,37.7947 43.446,37.672 43.0087,37.672C42.5607,37.672 42.182,37.7947 41.8727,38.04C41.574,38.2747 41.4247,38.6 41.4247,39.016C41.4247,39.4213 41.574,39.752 41.8727,40.008C42.182,40.2533 42.5607,40.376 43.0087,40.376ZM43.0087,45.32C43.5527,45.32 43.9954,45.176 44.3367,44.888C44.6887,44.6 44.8647,44.2213 44.8647,43.752C44.8647,43.304 44.6887,42.936 44.3367,42.648C43.9847,42.36 43.542,42.216 43.0087,42.216C42.4754,42.216 42.0274,42.36 41.6647,42.648C41.3127,42.936 41.1367,43.304 41.1367,43.752C41.1367,44.2213 41.3127,44.6 41.6647,44.888C42.0167,45.176 42.4647,45.32 43.0087,45.32Z"
+      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_high.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml
new file mode 100644
index 0000000..0d02e85
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml
@@ -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.
+  -->
+
+<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="100">
+  <path
+      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,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,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
new file mode 100644
index 0000000..b95b31a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_low.xml
@@ -0,0 +1,33 @@
+<!--
+  ~ 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="100">
+  <path
+      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,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,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_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml
new file mode 100644
index 0000000..cbf3fd1
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml
@@ -0,0 +1,34 @@
+<!--
+  ~ 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="100">
+  <path
+      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,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,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_power_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_off.xml
new file mode 100644
index 0000000..aef5c8a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_off.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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="M26.0001,4H22.0001V24H26.0001V4ZM33.9201,14.1L36.7401,11.28C43.7601,18.32 43.7601,29.7 36.7401,36.72C29.7201,43.76 18.3201,43.76 11.3001,36.74C4.2601,29.7 4.2601,18.3 11.2801,11.28L14.1001,14.08C8.6401,19.54 8.6601,28.44 14.1201,33.9C19.5601,39.34 28.4401,39.34 33.9001,33.88C39.3601,28.42 39.3801,19.56 33.9201,14.1Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"
+      android:fillType="evenOdd"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_on.xml
new file mode 100644
index 0000000..359ab84
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_on.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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="M26.0001,4H22.0001V24H26.0001V4ZM33.9201,14.1L36.7401,11.28C43.7601,18.32 43.7601,29.7 36.7401,36.72C29.7201,43.76 18.3201,43.76 11.3001,36.74C4.2601,29.7 4.2601,18.3 11.2801,11.28L14.1001,14.08C8.6401,19.54 8.6601,28.44 14.1201,33.9C19.5601,39.34 28.4401,39.34 33.9001,33.88C39.3601,28.42 39.3801,19.56 33.9201,14.1Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"
+      android:fillType="evenOdd"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_off.xml
new file mode 100644
index 0000000..dbe02ed
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_off.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.
+  -->
+
+<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="M12,27C12,23.14 15.5,20 19.8,20H32.34L27.16,25.18L30,28L40,18L30,8L27.18,10.82L32.34,16H19.8C13.3,16 8,20.94 8,27C8,33.06 13.3,38 19.8,38H34V34H19.8C15.5,34 12,30.86 12,27Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_on.xml
new file mode 100644
index 0000000..55aa087
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_on.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.
+  -->
+
+<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="M12,27C12,23.14 15.5,20 19.8,20H32.34L27.16,25.18L30,28L40,18L30,8L27.18,10.82L32.34,16H19.8C13.3,16 8,20.94 8,27C8,33.06 13.3,38 19.8,38H34V34H19.8C15.5,34 12,30.86 12,27Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_off.xml
new file mode 100644
index 0000000..97698d7
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_off.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="M33.6001,24.0001L40.8001,16.8001L33.6001,9.6001V14.4001H9.6001V19.2001H33.6001V24.0001Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+  <path
+      android:pathData="M14.4002,24L7.2002,31.2L14.4002,38.4V33.6H38.4002V28.8H14.4002V24Z"
+      android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_on.xml
new file mode 100644
index 0000000..ab3b3ab
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_on.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="M33.6001,24.0001L40.8001,16.8001L33.6001,9.6001V14.4001H9.6001V19.2001H33.6001V24.0001Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+  <path
+      android:pathData="M14.4002,24L7.2002,31.2L14.4002,38.4V33.6H38.4002V28.8H14.4002V24Z"
+      android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/nav_bar_button_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/nav_bar_button_background.xml
new file mode 100644
index 0000000..7c8b669
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/nav_bar_button_background.xml
@@ -0,0 +1,56 @@
+<!--
+  ~ 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:aapt="http://schemas.android.com/aapt">
+    <item android:state_selected="true">
+        <layer-list>
+            <item>
+                <ripple android:color="@color/car_ui_ripple_color">
+                    <item>
+                        <shape android:shape="rectangle">
+                            <size android:width="@dimen/system_bar_button_size"
+                                  android:height="@dimen/system_bar_button_size"/>
+                            <corners
+                                android:radius="@dimen/system_bar_button_corner_radius"/>
+                            <solid
+                                android:color="?android:attr/colorAccent"/>
+                        </shape>
+                    </item>
+                </ripple>
+            </item>
+            <item android:drawable="@drawable/ic_minimize"
+                  android:gravity="center"/>
+        </layer-list>
+    </item>
+    <item>
+        <layer-list>
+            <item>
+                <ripple android:color="@color/car_ui_ripple_color">
+                    <item>
+                        <shape android:shape="rectangle">
+                            <size android:width="@dimen/system_bar_button_size"
+                                  android:height="@dimen/system_bar_button_size"/>
+                            <corners
+                                android:radius="@dimen/system_bar_button_corner_radius"/>
+                            <solid
+                                android:color="@color/car_nav_icon_background_color"/>
+                        </shape>
+                    </item>
+                </ripple>
+            </item>
+        </layer-list>
+    </item>
+</selector>
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
new file mode 100644
index 0000000..69c3d30
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_bottom_system_bar.xml
@@ -0,0 +1,130 @@
+<?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.
+  -->
+
+<com.android.systemui.car.systembar.CarSystemBarView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/system_bar_background"
+    android:gravity="center"
+    android:orientation="horizontal">
+
+    <LinearLayout
+        android:id="@+id/nav_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <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>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_gravity="center"
+            android:gravity="center"
+            android:layoutDirection="ltr">
+
+            <com.android.systemui.car.systembar.CarSystemBarButton
+                android:id="@+id/grid_nav"
+                style="@style/SystemBarButton"
+                systemui:componentNames="com.android.car.carlauncher/.AppGridActivity"
+                systemui:icon="@drawable/car_ic_apps"
+                systemui:highlightWhenSelected="true"
+                systemui:toggleSelected="true"
+                systemui:intent="intent:#Intent;component=com.android.car.carlauncher/.AppGridActivity;launchFlags=0x24000000;end"
+                systemui:clearBackStack="true"/>
+
+            <com.android.systemui.car.systembar.CarSystemBarButton
+                android:id="@+id/standalone_notifications"
+                style="@style/SystemBarButton"
+                systemui:componentNames="com.android.car.notification/.CarNotificationCenterActivity"
+                systemui:packages="com.android.car.notification"
+                systemui:icon="@drawable/car_ic_notification"
+                systemui:highlightWhenSelected="true"
+                systemui:toggleSelected="true"
+                systemui:intent="intent:#Intent;component=com.android.car.notification/.CarNotificationCenterActivity;launchFlags=0x24000000;end"
+                systemui:longIntent="intent:#Intent;component=com.android.car.bugreport/.BugReportActivity;end"/>
+
+            <com.android.systemui.car.systembar.CarSystemBarButton
+                android:id="@+id/hvac"
+                style="@style/SystemBarButton"
+                systemui:icon="@drawable/car_ic_hvac"
+                systemui:highlightWhenSelected="true"
+                systemui:broadcast="true"/>
+
+            <com.android.systemui.car.systembar.AssitantButton
+                android:id="@+id/assist"
+                style="@style/SystemBarButton"
+                systemui:icon="@drawable/car_ic_mic"
+                systemui:highlightWhenSelected="true"
+                systemui:useDefaultAppIconForRole="true"/>
+        </LinearLayout>
+
+        <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>
+
+    <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/car_top_system_bar.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_top_system_bar.xml
new file mode 100644
index 0000000..e30ec45
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_top_system_bar.xml
@@ -0,0 +1,85 @@
+<?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.
+  -->
+
+<com.android.systemui.car.systembar.CarSystemBarView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/car_top_bar"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/status_bar_background"
+    android:gravity="center"
+    android:orientation="horizontal">
+
+    <com.android.systemui.car.systembar.CarSystemBarButton
+        android:id="@+id/user_name"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginStart="@dimen/car_padding_3"
+        android:layout_gravity="start"
+        android:gravity="start"
+        android:orientation="horizontal"
+        systemui:intent="intent:#Intent;component=com.android.car.settings/.profiles.ProfileSwitcherActivity;launchFlags=0x24000000;end">
+            <ImageView
+                android:id="@+id/user_avatar"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:src="@drawable/car_ic_user_icon"
+                android:layout_marginEnd="@dimen/system_bar_user_icon_padding"/>
+            <TextView
+                android:id="@+id/user_name_text"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:gravity="center_vertical"
+                android:textAppearance="@style/TextAppearance.SystemBar.Username"
+                android:maxLines="1"
+                android:maxLength="10"/>
+    </com.android.systemui.car.systembar.CarSystemBarButton>
+
+    <com.android.systemui.statusbar.policy.Clock
+        android:id="@+id/clock"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:layout_gravity="center"
+        android:gravity="center"
+        android:paddingStart="@dimen/car_padding_2"
+        android:paddingEnd="@dimen/car_padding_2"
+        android:elevation="5dp"
+        android:singleLine="true"
+        android:textAppearance="@style/TextAppearance.SystemBar.Clock"
+        systemui:amPmStyle="normal"/>
+
+    <com.android.systemui.car.systembar.CarSystemBarButton
+        android:id="@+id/system_icon_area"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginEnd="@dimen/car_padding_3"
+        android:layout_gravity="end"
+        android:gravity="end"
+        systemui:intent="intent:#Intent;component=com.android.car.settings/.common.CarSettingActivities$HomepageActivity;launchFlags=0x24000000;end">
+
+        <com.android.systemui.statusbar.phone.StatusIconContainer
+            android:id="@+id/statusIcons"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scaleType="fitCenter"
+            android:gravity="center"
+            android:orientation="horizontal"
+        />
+    </com.android.systemui.car.systembar.CarSystemBarButton>
+</com.android.systemui.car.systembar.CarSystemBarView>
\ No newline at end of file
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
new file mode 100644
index 0000000..1770796
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/fan_direction.xml
@@ -0,0 +1,110 @@
+<!--
+  ~ 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.
+  -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:systemui="http://schemas.android.com/apk/res-auto"
+       android:layout_width="match_parent"
+       android:layout_height="match_parent">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
+            android:id="@+id/direction_face"
+            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"
+            systemui:hvacAreaId="117"
+            systemui:hvacPropertyId="356517121"
+            systemui:hvacToggleOffButtonDrawable="@drawable/ic_airflow_head_off"
+            systemui:hvacToggleOnButtonDrawable="@drawable/ic_airflow_head_on"
+            systemui:hvacTurnOffIfAutoOn="true"
+            systemui:onValue="1"
+            systemui:offValue="0"
+            systemui:preventToggleOff="true"/>
+        <View
+            android:layout_width="32dp"
+            android:layout_height="match_parent"/>
+        <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
+            android:id="@+id/direction_floor"
+            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"
+            systemui:hvacAreaId="117"
+            systemui:hvacPropertyId="356517121"
+            systemui:hvacToggleOffButtonDrawable="@drawable/ic_airflow_feet_off"
+            systemui:hvacToggleOnButtonDrawable="@drawable/ic_airflow_feet_on"
+            systemui:hvacTurnOffIfAutoOn="true"
+            systemui:onValue="2"
+            systemui:offValue="0"
+            systemui:preventToggleOff="true"/>
+        <View
+            android:layout_width="32dp"
+            android:layout_height="match_parent"/>
+        <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
+            android:id="@+id/direction_defrost_front_and_floor"
+            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"
+            systemui:hvacAreaId="117"
+            systemui:hvacPropertyId="356517121"
+            systemui:hvacToggleOffButtonDrawable="@drawable/ic_airflow_windshield_off"
+            systemui:hvacToggleOnButtonDrawable="@drawable/ic_airflow_windshield_on"
+            systemui:hvacTurnOffIfAutoOn="true"
+            systemui:onValue="6"
+            systemui:offValue="0"
+            systemui:preventToggleOff="true"/>
+    </LinearLayout>
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="32dp"/>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+            android:id="@+id/direction_defrost_front"
+            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"
+            systemui:hvacAreaId="1"
+            systemui:hvacPropertyId="320865540"
+            systemui:hvacToggleOffButtonDrawable="@drawable/ic_defroster_windshield_off"
+            systemui:hvacToggleOnButtonDrawable="@drawable/ic_defroster_windshield_on"
+            systemui:hvacTurnOffIfAutoOn="true"
+            systemui:onValue="1"
+            systemui:offValue="0"/>
+        <View
+            android:layout_width="32dp"
+            android:layout_height="match_parent"/>
+        <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+            android:id="@+id/direction_defrost_rear"
+            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"
+            systemui:hvacAreaId="2"
+            systemui:hvacPropertyId="320865540"
+            systemui:hvacToggleOffButtonDrawable="@drawable/ic_defroster_rear_off"
+            systemui:hvacToggleOnButtonDrawable="@drawable/ic_defroster_rear_on"
+            systemui:hvacTurnOffIfAutoOn="true"
+            systemui:onValue="1"
+            systemui:offValue="0"/>
+    </LinearLayout>
+</merge>
\ No newline at end of file
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
new file mode 100644
index 0000000..86df240
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_container.xml
@@ -0,0 +1,192 @@
+<?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.
+  -->
+
+<com.android.car.ui.FocusArea
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/hvac_panel_container"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/hvac_panel_full_expanded_height"
+    android:layout_gravity="bottom">
+    <com.android.systemui.car.hvac.HvacPanelView
+        android:id="@+id/hvac_panel"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/hvac_panel_bg">
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/top_guideline"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:orientation="horizontal"
+            app:layout_constraintGuide_begin="@dimen/hvac_panel_buttons_guideline"/>
+
+        <!-- ************************ -->
+        <!-- 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_long_button_dimen"
+            android:layout_marginLeft="@dimen/hvac_panel_button_external_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
+            android:background="@drawable/hvac_default_background"
+            app:layout_constraintLeft_toLeftOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/top_guideline"
+            systemui:hvacAreaId="117"
+            systemui:hvacPropertyId="354419973"
+            systemui:hvacToggleOffButtonDrawable="@drawable/ic_ac_off"
+            systemui:hvacToggleOnButtonDrawable="@drawable/ic_ac_on"
+            systemui:hvacTurnOffIfAutoOn="true"/>
+
+        <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_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_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_long_button_dimen"
+            android:layout_height="@dimen/hvac_panel_button_dimen"
+            android:layout_marginBottom="@dimen/hvac_panel_button_external_bottom_margin"
+            android:layout_marginLeft="@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_constraintLeft_toLeftOf="parent"
+            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 group of buttons. -->
+        <!-- ************************ -->
+
+        <LinearLayout
+            android:id="@+id/airflow_group"
+            android:layout_width="@dimen/hvac_panel_slider_width"
+            android:layout_height="0dp"
+            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_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="@dimen/hvac_panel_slider_width"
+            android:layout_height="@dimen/hvac_panel_button_dimen"
+            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"
+            android:progressDrawable="@drawable/fan_speed_seek_bar"
+            android:thumb="@drawable/fan_speed_seek_bar_thumb"
+            android:maxHeight="@dimen/hvac_panel_button_dimen"
+            android:minHeight="@dimen/hvac_panel_button_dimen"
+            android:background="@drawable/fan_speed_seek_bar_background"
+            android:splitTrack="false"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintLeft_toLeftOf="@+id/airflow_group"
+            app:layout_constraintRight_toRightOf="@+id/airflow_group"
+            app:layout_constraintTop_toBottomOf="@+id/airflow_group"/>
+
+        <!-- ************************* -->
+        <!-- Third group of buttons. -->
+        <!-- ************************* -->
+
+        <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_long_button_dimen"
+            android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
+            android:background="@drawable/hvac_heat_background"
+            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"/>
+
+        <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_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_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/top_guideline"
+            systemui:hvacAreaId="117"
+            systemui:hvacPropertyId="354419976"
+            systemui:hvacTurnOffIfAutoOn="true"
+            systemui:hvacToggleOnButtonDrawable="@drawable/ic_recirculate_on"
+            systemui:hvacToggleOffButtonDrawable="@drawable/ic_recirculate_off"/>
+
+        <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_marginRight="@dimen/hvac_panel_button_external_margin"
+            android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
+            android:background="@drawable/hvac_default_background"
+            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"/>
+
+        <include
+            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
new file mode 100644
index 0000000..62a1e81
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_handle_bar.xml
@@ -0,0 +1,37 @@
+<?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.
+  -->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <FrameLayout
+        android:id="@+id/handle_bar"
+        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/layout/text_toast.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/text_toast.xml
new file mode 100644
index 0000000..87eb12c
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/text_toast.xml
@@ -0,0 +1,45 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:maxWidth="@*android:dimen/toast_width"
+    android:background="@android:drawable/toast_frame"
+    android:elevation="@*android:dimen/toast_elevation"
+    android:paddingEnd="@dimen/toast_margin"
+    android:paddingTop="@dimen/toast_margin"
+    android:paddingBottom="@dimen/toast_margin"
+    android:paddingStart="@dimen/toast_margin"
+    android:layout_marginBottom="@dimen/toast_bottom_margin">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="@dimen/toast_icon_dimen"
+        android:layout_height="@dimen/toast_icon_dimen"
+        android:layout_marginEnd="@dimen/toast_margin"/>
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="2"
+        android:textAppearance="@*android:style/TextAppearance.Toast"/>
+</LinearLayout>
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
new file mode 100644
index 0000000..828003f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/arrays.xml
@@ -0,0 +1,39 @@
+<?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>
+    <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_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_high</item>
+    </array>
+    <array name="hvac_fan_speed_icons">
+        <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>
+        <item>@drawable/fan_speed_seek_bar_thumb_4</item>
+        <item>@drawable/fan_speed_seek_bar_thumb_5</item>
+        <item>@drawable/fan_speed_seek_bar_thumb_6</item>
+        <item>@drawable/fan_speed_seek_bar_thumb_7</item>
+        <item>@drawable/fan_speed_seek_bar_thumb_8</item>
+    </array>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/attrs.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/attrs.xml
new file mode 100644
index 0000000..120905f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/attrs.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ 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>
+    <declare-styleable name="FanSpeedSeekBar">
+        <!-- List of drawables that will be shown when the seat heat level button is clicked.
+             This list should have exactly R.integer.hvac_seat_heat_level_count items.
+             The first item should have the "off" drawable. -->
+        <attr name="fanSpeedThumbIcons" format="reference"/>
+    </declare-styleable>
+</resources>
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
new file mode 100644
index 0000000..3f62d35
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/colors.xml
@@ -0,0 +1,46 @@
+<?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">
+    <color name="rear_view_camera_button_background">#CCFFFFFF</color>
+    <color name="rear_view_camera_exit_icon_color">@android:color/black</color>
+
+    <color name="car_nav_icon_fill_color">#FFFFFF</color>
+    <color name="car_nav_icon_background_color">#282A2D</color>
+    <color name="car_nav_minimize_icon_fill_color">@android:color/black</color>
+
+    <drawable name="system_bar_background">#000000</drawable>
+    <color name="system_bar_text_color">#E8EAED</color>
+    <color name="hvac_temperature_adjust_button_color">#282A2D</color>
+    <color name="hvac_temperature_decrease_arrow_color">#E8EAED</color>
+    <color name="hvac_temperature_increase_arrow_color">#E8EAED</color>
+
+    <color name="status_bar_background_color">#00000000</color>
+    <drawable name="status_bar_background">@color/status_bar_background_color</drawable>
+
+    <color name="hvac_background_color">#202124</color>
+    <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">#6BF0FF</color>
+    <color name="hvac_on_heating_background_color">#EE675C</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
new file mode 100644
index 0000000..75918eb
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/config.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.
+  -->
+
+<resources>
+    <string name="config_systemUIFactoryComponent" translatable="false">
+        com.android.systemui.CarUiPortraitSystemUIFactory
+    </string>
+
+    <!-- Car System UI's OverlayViewsMediator.
+         Whenever a new class is added, make sure to also add that class to OverlayWindowModule. -->
+    <string-array name="config_carSystemUIOverlayViewsMediators" translatable="false">
+        <item>com.android.systemui.car.hvac.AutoDismissHvacPanelOverlayViewMediator</item>
+        <item>com.android.systemui.car.keyguard.CarKeyguardViewMediator</item>
+        <item>com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator</item>
+        <item>com.android.systemui.car.userswitcher.UserSwitchTransitionViewMediator</item>
+    </string-array>
+
+    <integer name="hvac_num_fan_speeds">8</integer>
+
+    <integer name="config_hvacAutoDismissDurationMs">15000</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
new file mode 100644
index 0000000..f900e57
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/dimens.xml
@@ -0,0 +1,82 @@
+<?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>
+    <!-- dimensions for rear view camera -->
+    <dimen name="rear_view_camera_width">1020dp</dimen>
+    <dimen name="rear_view_camera_height">720dp</dimen>
+
+    <dimen name="rear_view_camera_exit_button_width">48dp</dimen>
+    <dimen name="rear_view_camera_exit_button_height">48dp</dimen>
+    <dimen name="rear_view_camera_exit_button_margin">24dp</dimen>
+    <dimen name="rear_view_camera_exit_icon_width">26dp</dimen>
+    <dimen name="rear_view_camera_exit_icon_height">26dp</dimen>
+
+    <dimen name="hvac_container_padding">24dp</dimen>
+    <dimen name="hvac_temperature_text_size">56sp</dimen>
+    <dimen name="hvac_temperature_text_padding">12dp</dimen>
+    <dimen name="hvac_temperature_button_size">64dp</dimen>
+
+    <dimen name="system_bar_icon_drawing_size">56dp</dimen>
+    <dimen name="system_bar_button_size">88dp</dimen>
+    <!-- Margin between the system bar buttons -->
+    <dimen name="system_bar_button_margin">40dp</dimen>
+    <!-- Padding between the system bar button and the icon within it -->
+    <dimen name="system_bar_button_padding">16dp</dimen>
+    <dimen name="system_bar_button_corner_radius">24dp</dimen>
+
+    <dimen name="system_bar_user_icon_drawing_size">44dp</dimen>
+    <dimen name="status_bar_system_icon_spacing">32dp</dimen>
+
+    <dimen name="system_bar_minimize_icon_height">17dp</dimen>
+    <dimen name="system_bar_minimize_icon_width">28dp</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">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">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">@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_button_dimen">88dp</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_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>
+    <dimen name="toast_icon_dimen">48dp</dimen>
+    <dimen name="toast_bottom_margin">32dp</dimen>
+
+</resources>
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/res/values/strings.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/strings.xml
new file mode 100644
index 0000000..c9cff07
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?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">
+    <!-- Format for temperature in the temperature control view (No decimal) -->
+    <string name="hvac_temperature_format_fahrenheit" translatable="false">%.0f</string>
+
+    <!-- HVAC panel header [CHAR LIMIT=40]-->
+    <string name="hvac_panel_header">Comfort controls</string>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/styles.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/styles.xml
new file mode 100644
index 0000000..b5f46f8
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/styles.xml
@@ -0,0 +1,40 @@
+<!--
+  ~ 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">
+
+    <!--
+        Note on selected/unselected icons:
+        The icon is always tinted with @color/car_nav_icon_fill_color_selected in @layout/car_system_bar_button
+        Unselected: keep this behavior so all icons have consistent color (eg. tint a multi-colored default app icon)
+        Selected: set selected alpha 0, making icon transparent. Use state list nav_bar_button_background to show selected icon (in addition to background).
+    -->
+    <style name="SystemBarButton">
+        <item name="android:layout_width">@dimen/system_bar_button_size</item>
+        <item name="android:layout_height">@dimen/system_bar_button_size</item>
+        <item name="android:background">@drawable/nav_bar_button_background</item>
+        <item name="android:gravity">center</item>
+        <item name="unselectedAlpha">1.0</item>
+        <item name="selectedAlpha">0</item>
+    </style>
+
+    <style name="TextAppearance.SystemBar.Username"
+           parent="@android:style/TextAppearance.DeviceDefault">
+        <item name="android:textSize">@dimen/car_body1_size</item>
+        <item name="android:textColor">@color/system_bar_text_color</item>
+    </style>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitGlobalRootComponent.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitGlobalRootComponent.java
new file mode 100644
index 0000000..af85885
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitGlobalRootComponent.java
@@ -0,0 +1,44 @@
+/*
+ * 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.systemui;
+
+import com.android.systemui.dagger.GlobalModule;
+import com.android.systemui.dagger.WMModule;
+
+import javax.inject.Singleton;
+
+import dagger.Component;
+
+/**
+ * Root Component for Dagger injection for CarUiPortraitSystemUI
+ */
+@Singleton
+@Component(
+        modules = {
+                GlobalModule.class,
+                CarUiPortraitSysUIComponentModule.class,
+                WMModule.class
+        })
+interface CarUiPortraitGlobalRootComponent extends CarGlobalRootComponent {
+    @Component.Builder
+    interface Builder extends CarGlobalRootComponent.Builder {
+        CarUiPortraitGlobalRootComponent build();
+    }
+
+    @Override
+    CarUiPortraitSysUIComponent.Builder getSysUIComponent();
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponent.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponent.java
new file mode 100644
index 0000000..25e488f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponent.java
@@ -0,0 +1,43 @@
+/*
+ * 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.systemui;
+
+import com.android.systemui.dagger.DependencyProvider;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.SystemUIModule;
+
+import dagger.Subcomponent;
+
+/**
+ * Dagger Subcomponent for Core SysUI.
+ */
+@SysUISingleton
+@Subcomponent(modules = {
+        CarComponentBinder.class,
+        DependencyProvider.class,
+        SystemUIModule.class,
+        CarSystemUIModule.class,
+        CarUiPortraitSystemUIBinder.class})
+public interface CarUiPortraitSysUIComponent extends CarSysUIComponent {
+    /**
+     * Builder for a CarSysUIComponent.
+     */
+    @Subcomponent.Builder
+    interface Builder extends CarSysUIComponent.Builder {
+        CarUiPortraitSysUIComponent build();
+    }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponentModule.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponentModule.java
new file mode 100644
index 0000000..196c917
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponentModule.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.systemui;
+
+import dagger.Binds;
+import dagger.Module;
+
+/**
+ * Dagger module for including the CarUiPortraitSysUIComponent.
+ *
+ * TODO(b/162923491): Remove or otherwise refactor this module. This is a stop gap.
+ */
+@Module(subcomponents = {CarUiPortraitSysUIComponent.class})
+public abstract class CarUiPortraitSysUIComponentModule {
+    @Binds
+    abstract CarGlobalRootComponent bindSystemUIRootComponent(
+            CarUiPortraitGlobalRootComponent systemUIRootComponent);
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIBinder.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIBinder.java
new file mode 100644
index 0000000..255d70e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIBinder.java
@@ -0,0 +1,26 @@
+/*
+ * 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.systemui;
+
+import com.android.systemui.car.window.ExtendedOverlayWindowModule;
+
+import dagger.Module;
+
+/** Binder for AAECarSystemUI specific {@link SystemUI} modules and components. */
+@Module(includes = {ExtendedOverlayWindowModule.class})
+abstract class CarUiPortraitSystemUIBinder extends CarSystemUIBinder {
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIFactory.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIFactory.java
new file mode 100644
index 0000000..ef75f48
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.systemui;
+
+import android.content.Context;
+
+import com.android.systemui.dagger.GlobalRootComponent;
+
+/**
+ * Class factory to provide AAECarSystemUI specific SystemUI components.
+ */
+public class CarUiPortraitSystemUIFactory extends CarSystemUIFactory {
+    @Override
+    protected GlobalRootComponent buildGlobalRootComponent(Context context) {
+        return DaggerCarUiPortraitGlobalRootComponent.builder().context(context).build();
+    }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewController.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewController.java
new file mode 100644
index 0000000..bf0b34d
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewController.java
@@ -0,0 +1,95 @@
+/*
+ * 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.systemui.car.hvac;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Handler;
+
+import com.android.systemui.R;
+import com.android.systemui.car.CarDeviceProvisionedController;
+import com.android.systemui.car.window.OverlayViewGlobalStateController;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.wm.shell.animation.FlingAnimationUtils;
+
+import javax.inject.Inject;
+
+/**
+ *  An extension of {@link HvacPanelOverlayViewController} which auto dismisses the panel if there
+ *  is no activity for some configured amount of time.
+ */
+@SysUISingleton
+public class AutoDismissHvacPanelOverlayViewController extends HvacPanelOverlayViewController {
+
+    private final Resources mResources;
+    private final Handler mHandler;
+
+    private HvacPanelView mHvacPanelView;
+    private int mAutoDismissDurationMs;
+
+    private final Runnable mAutoDismiss = () -> {
+        if (isPanelExpanded()) {
+            toggle();
+        }
+    };
+
+    @Inject
+    public AutoDismissHvacPanelOverlayViewController(Context context,
+            @Main Resources resources,
+            HvacController hvacController,
+            OverlayViewGlobalStateController overlayViewGlobalStateController,
+            FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
+            CarDeviceProvisionedController carDeviceProvisionedController,
+            @Main Handler handler) {
+        super(context, resources, hvacController, overlayViewGlobalStateController,
+                flingAnimationUtilsBuilder, carDeviceProvisionedController);
+        mResources = resources;
+        mHandler = handler;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mAutoDismissDurationMs = mResources.getInteger(R.integer.config_hvacAutoDismissDurationMs);
+
+        mHvacPanelView = getLayout().findViewById(R.id.hvac_panel);
+        mHvacPanelView.setMotionEventHandler(event -> {
+            if (!isPanelExpanded()) {
+                return;
+            }
+
+            mHandler.removeCallbacks(mAutoDismiss);
+            mHandler.postDelayed(mAutoDismiss, mAutoDismissDurationMs);
+        });
+    }
+
+    @Override
+    protected void onAnimateExpandPanel() {
+        super.onAnimateExpandPanel();
+
+        mHandler.postDelayed(mAutoDismiss, mAutoDismissDurationMs);
+    }
+
+    @Override
+    protected void onAnimateCollapsePanel() {
+        super.onAnimateCollapsePanel();
+
+        mHandler.removeCallbacks(mAutoDismiss);
+    }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewMediator.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewMediator.java
new file mode 100644
index 0000000..ce30c43
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewMediator.java
@@ -0,0 +1,39 @@
+/*
+ * 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.systemui.car.hvac;
+
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.car.systembar.CarSystemBarController;
+import com.android.systemui.dagger.SysUISingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Instance of {@link HvacPanelOverlayViewMediator} which uses {@link
+ * AutoDismissHvacPanelOverlayViewController}.
+ */
+@SysUISingleton
+public class AutoDismissHvacPanelOverlayViewMediator extends HvacPanelOverlayViewMediator {
+
+    @Inject
+    public AutoDismissHvacPanelOverlayViewMediator(
+            CarSystemBarController carSystemBarController,
+            AutoDismissHvacPanelOverlayViewController hvacPanelOverlayViewController,
+            BroadcastDispatcher broadcastDispatcher) {
+        super(carSystemBarController, hvacPanelOverlayViewController, broadcastDispatcher);
+    }
+}
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
new file mode 100644
index 0000000..bc7f4f2
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/custom/FanSpeedSeekBar.java
@@ -0,0 +1,217 @@
+/*
+ * 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.systemui.car.hvac.custom;
+
+import static android.car.VehiclePropertyIds.HVAC_AUTO_ON;
+import static android.car.VehiclePropertyIds.HVAC_FAN_SPEED;
+import static android.car.VehiclePropertyIds.HVAC_POWER_ON;
+
+import android.car.hardware.CarPropertyValue;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.widget.SeekBar;
+
+import androidx.annotation.ArrayRes;
+
+import com.android.systemui.R;
+import com.android.systemui.car.hvac.HvacController;
+import com.android.systemui.car.hvac.HvacPropertySetter;
+import com.android.systemui.car.hvac.HvacView;
+
+/** Custom seek bar to control fan speed. */
+public class FanSpeedSeekBar extends SeekBar implements HvacView {
+
+    private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
+    private static final String TAG = "FanSpeedSeekBar";
+
+    private final SparseArray<Drawable> mIcons = new SparseArray<>();
+
+    private HvacPropertySetter mHvacPropertySetter;
+    private int mHvacGlobalAreaId;
+
+    private boolean mPowerOn;
+    private boolean mAutoOn;
+
+    private float mOnAlpha;
+    private float mOffAlpha;
+
+    private final OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() {
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            int prevProgress = getProgress();
+            // Limit updates to the hvac property to be only those that come from the user in order
+            // to avoid an infinite loop.
+            if (shouldAllowControl() && fromUser && progress == prevProgress) {
+                mHvacPropertySetter.setHvacProperty(HVAC_FAN_SPEED, getAreaId(), progress);
+            } else if (progress != prevProgress) {
+                // There is an edge case with seek bar touch handling that can lead to an
+                // inconsistent state of the progress state and UI. We need to set the progress to
+                // a different value before setting it to the value we expect in order to ensure
+                // that the update doesn't get dropped.
+                setProgress(progress);
+                setProgress(prevProgress);
+                updateUI();
+            }
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+            // no-op.
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+            // no-op.
+        }
+    };
+
+    public FanSpeedSeekBar(Context context) {
+        super(context);
+        init(null);
+    }
+
+    public FanSpeedSeekBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(attrs);
+    }
+
+    public FanSpeedSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(attrs);
+    }
+
+    public FanSpeedSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(attrs);
+    }
+
+    private void init(AttributeSet attrs) {
+        int speeds = mContext.getResources().getInteger(R.integer.hvac_num_fan_speeds);
+        if (speeds < 1) {
+            throw new IllegalArgumentException("The nuer of fan speeds should be > 1");
+        }
+
+        setMin(1);
+        incrementProgressBy(1);
+        setMax(speeds);
+        int thumbRadius = mContext.getResources().getDimensionPixelSize(
+                R.dimen.hvac_panel_seek_bar_radius);
+        setPadding(thumbRadius, 0, thumbRadius, 0);
+        mHvacGlobalAreaId = mContext.getResources().getInteger(R.integer.hvac_global_area_id);
+
+        mOnAlpha = mContext.getResources().getFloat(R.dimen.hvac_turned_on_alpha);
+        mOffAlpha = mContext.getResources().getFloat(R.dimen.hvac_turned_off_alpha);
+
+        if (attrs == null) {
+            return;
+        }
+
+        // Get fan speed thumb drawables.
+        TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.FanSpeedSeekBar);
+        @ArrayRes int drawableListRes = typedArray.getResourceId(
+                R.styleable.FanSpeedSeekBar_fanSpeedThumbIcons,
+                R.array.hvac_fan_speed_icons);
+
+        TypedArray fanSpeedThumbIcons = mContext.getResources().obtainTypedArray(drawableListRes);
+        if (fanSpeedThumbIcons.length() != speeds) {
+            throw new IllegalArgumentException(
+                    "R.styeable.SeatHeatLevelButton_seatHeaterIconDrawableList should have the "
+                            + "same length as R.integer.hvac_seat_heat_level_count");
+        }
+
+        for (int i = 0; i < speeds; i++) {
+            mIcons.set(i + 1, fanSpeedThumbIcons.getDrawable(i));
+        }
+        fanSpeedThumbIcons.recycle();
+        typedArray.recycle();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        setOnSeekBarChangeListener(mSeekBarChangeListener);
+    }
+
+    @Override
+    public void setHvacPropertySetter(HvacPropertySetter hvacPropertySetter) {
+        mHvacPropertySetter = hvacPropertySetter;
+    }
+
+    @Override
+    public void onHvacTemperatureUnitChanged(boolean usesFahrenheit) {
+        // no-op.
+    }
+
+    @Override
+    public void onPropertyChanged(CarPropertyValue value) {
+        if (value == null) {
+            if (DEBUG) {
+                Log.w(TAG, "onPropertyChanged: received null value");
+            }
+            return;
+        }
+
+        if (DEBUG) {
+            Log.w(TAG, "onPropertyChanged: property id: " + value.getPropertyId());
+            Log.w(TAG, "onPropertyChanged: area id: " + value.getAreaId());
+            Log.w(TAG, "onPropertyChanged: value: " + value.getValue());
+        }
+
+        if (value.getPropertyId() == HVAC_FAN_SPEED) {
+            int level = (int) value.getValue();
+            setProgress(level, /* animate= */ true);
+        }
+
+        if (value.getPropertyId() == HVAC_POWER_ON) {
+            mPowerOn = (boolean) value.getValue();
+        }
+
+        if (value.getPropertyId() == HVAC_AUTO_ON) {
+            mAutoOn = (boolean) value.getValue();
+        }
+
+        updateUI();
+    }
+
+    @Override
+    public @HvacController.HvacProperty Integer getHvacPropertyToView() {
+        return HVAC_FAN_SPEED;
+    }
+
+    @Override
+    public @HvacController.AreaId Integer getAreaId() {
+        return mHvacGlobalAreaId;
+    }
+
+    private void updateUI() {
+        int progress = getProgress();
+        setThumb(mIcons.get(progress));
+        setSelected(progress > 0);
+        setAlpha(shouldAllowControl() ? mOnAlpha : mOffAlpha);
+        // Steal touch events if shouldn't allow control.
+        setOnTouchListener(shouldAllowControl() ? null : (v, event) -> true);
+    }
+
+    private boolean shouldAllowControl() {
+        return mPowerOn && !mAutoOn;
+    }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/window/ExtendedOverlayWindowModule.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/window/ExtendedOverlayWindowModule.java
new file mode 100644
index 0000000..21f1500
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/window/ExtendedOverlayWindowModule.java
@@ -0,0 +1,36 @@
+/*
+ * 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.systemui.car.window;
+
+import com.android.systemui.car.hvac.AutoDismissHvacPanelOverlayViewMediator;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
+
+/** Lists additional {@link OverlayViewMediator} that apply to the CarUiPortraitSystemUI. */
+@Module
+public abstract class ExtendedOverlayWindowModule {
+
+    /** Injects RearViewCameraViewMediator. */
+    @Binds
+    @IntoMap
+    @ClassKey(AutoDismissHvacPanelOverlayViewMediator.class)
+    public abstract OverlayViewMediator bindAutoDismissHvacPanelViewMediator(
+            AutoDismissHvacPanelOverlayViewMediator overlayViewsMediator);
+}
diff --git a/car_product/car_ui_portrait/apps/HideApps/Android.mk b/car_product/car_ui_portrait/apps/HideApps/Android.mk
new file mode 100644
index 0000000..74cdcaa
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/HideApps/Android.mk
@@ -0,0 +1,34 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+LOCAL_PACKAGE_NAME := CarUiPortraitHideApps
+LOCAL_SDK_VERSION := current
+
+# Add packages here to remove them from the build
+LOCAL_OVERRIDES_PACKAGES := \
+    CarRotaryController \
+    RotaryPlayground \
+    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/apps/HideApps/AndroidManifest.xml b/car_product/car_ui_portrait/apps/HideApps/AndroidManifest.xml
new file mode 100644
index 0000000..a234af5
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/HideApps/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.android.car.caruiportrait.hideapps">
+    <application
+        android:allowBackup="false"
+        android:debuggable="false"
+        android:label="CarUiPortraitHideApps">
+    </application>
+</manifest>
diff --git a/car_product/car_ui_portrait/apps/car_ui_portrait_apps.mk b/car_product/car_ui_portrait/apps/car_ui_portrait_apps.mk
new file mode 100644
index 0000000..8e01ccc
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/car_ui_portrait_apps.mk
@@ -0,0 +1,26 @@
+#
+# 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.
+#
+
+# All apps that should be included in CarUiPortrait builds
+PRODUCT_PACKAGES += \
+    CarUiPortraitSettings \
+    CarUiPortraitSystemUI \
+    CarNotification \
+    PaintBooth \
+
+# All apps to be excluded in car_ui_portrait builds should be specified as part of CarUiPortraitHideApps.
+PRODUCT_PACKAGES += \
+    CarUiPortraitHideApps
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/bootanimation/README b/car_product/car_ui_portrait/bootanimation/README
new file mode 100644
index 0000000..2ce687f
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/README
@@ -0,0 +1,68 @@
+# Boot Animation
+
+The boot animation format is described in full by [FORMAT.md](https://android.googlesource.com/platform/frameworks/base/+/master/cmds/bootanimation/FORMAT.md)
+
+## Command to create the zip:
+zip -0qry -i \*.txt \*.png \*.wav @ ../bootanimation.zip *.txt part*
+
+## zipfile layout
+
+The `bootanimation.zip` archive file includes:
+
+    desc.txt - a text file
+    part0  \
+    part1   \  directories full of PNG frames
+    ...     /
+    partN  /
+
+## desc.txt format
+
+The first line defines the general parameters of the animation:
+
+    WIDTH HEIGHT FPS [PROGRESS]
+
+  * **WIDTH:** animation width (pixels)
+  * **HEIGHT:** animation height (pixels)
+  * **FPS:** frames per second, e.g. 60
+  * **PROGRESS:** whether to show a progress percentage on the last part
+      + The percentage will be displayed with an x-coordinate of 'c', and a
+        y-coordinate set to 1/3 of the animation height.
+
+It is followed by a number of rows of the form:
+
+    TYPE COUNT PAUSE PATH [FADE [#RGBHEX [CLOCK1 [CLOCK2]]]]
+
+  * **TYPE:** a single char indicating what type of animation segment this is:
+      + `p` -- this part will play unless interrupted by the end of the boot
+      + `c` -- this part will play to completion, no matter what
+      + `f` -- same as `p` but in addition the specified number of frames is being faded out while
+        continue playing. Only the first interrupted `f` part is faded out, other subsequent `f`
+        parts are skipped
+  * **COUNT:** how many times to play the animation, or 0 to loop forever until boot is complete
+  * **PAUSE:** number of FRAMES to delay after this part ends
+  * **PATH:** directory in which to find the frames for this part (e.g. `part0`)
+  * **FADE:** _(ONLY FOR `f` TYPE)_ number of frames to fade out when interrupted where `0` means
+              _immediately_ which makes `f ... 0` behave like `p` and doesn't count it as a fading
+              part
+  * **RGBHEX:** _(OPTIONAL)_ a background color, specified as `#RRGGBB`
+  * **CLOCK1, CLOCK2:** _(OPTIONAL)_ the coordinates at which to draw the current time (for watches):
+      + If only `CLOCK1` is provided it is the y-coordinate of the clock and the x-coordinate
+        defaults to `c`
+      + If both `CLOCK1` and `CLOCK2` are provided then `CLOCK1` is the x-coordinate and `CLOCK2` is
+        the y-coodinate
+      + Values can be either a positive integer, a negative integer, or `c`
+          - `c` -- will centre the text
+          - `n` -- will position the text n pixels from the start; left edge for x-axis, bottom edge
+            for y-axis
+          - `-n` -- will position the text n pixels from the end; right edge for x-axis, top edge
+            for y-axis
+          - Examples:
+              * `-24` or `c -24` will position the text 24 pixels from the top of the screen,
+                centred horizontally
+              * `16 c` will position the text 16 pixels from the left of the screen, centred
+                vertically
+              * `-32 32` will position the text such that the bottom right corner is 32 pixels above
+                and 32 pixels left of the edges of the screen
+
+There is also a special TYPE, `$SYSTEM`, that loads `/system/media/bootanimation.zip`
+and plays that.
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/bootanimation/bootanimation.zip b/car_product/car_ui_portrait/bootanimation/bootanimation.zip
new file mode 100644
index 0000000..2843730
--- /dev/null
+++ 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
new file mode 100644
index 0000000..abfa895
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/desc.txt
@@ -0,0 +1,3 @@
+1224 2175 24
+c 1 0 part0
+p 0 0 part1
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00023.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00023.png
new file mode 100644
index 0000000..29499a6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00023.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00024.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00024.png
new file mode 100644
index 0000000..29499a6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00024.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00025.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00025.png
new file mode 100644
index 0000000..7d1314e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00025.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00026.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00026.png
new file mode 100644
index 0000000..f50bbda
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00026.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00027.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00027.png
new file mode 100644
index 0000000..2a90baf
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00027.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00028.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00028.png
new file mode 100644
index 0000000..02ed17b
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00028.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00029.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00029.png
new file mode 100644
index 0000000..8a22023
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00029.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00030.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00030.png
new file mode 100644
index 0000000..6cf674b
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00030.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00031.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00031.png
new file mode 100644
index 0000000..482193d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00031.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00032.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00032.png
new file mode 100644
index 0000000..aa224f8
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00032.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00033.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00033.png
new file mode 100644
index 0000000..4cd9877
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00033.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00034.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00034.png
new file mode 100644
index 0000000..af5ab61
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00034.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00035.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00035.png
new file mode 100644
index 0000000..691c0df
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00035.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00036.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00036.png
new file mode 100644
index 0000000..ec45285
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00036.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00037.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00037.png
new file mode 100644
index 0000000..8962084
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00037.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00038.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00038.png
new file mode 100644
index 0000000..1640075
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00038.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00039.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00039.png
new file mode 100644
index 0000000..130924a
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00039.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00040.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00040.png
new file mode 100644
index 0000000..19802da
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00040.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00041.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00041.png
new file mode 100644
index 0000000..dfd77a0
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00041.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00042.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00042.png
new file mode 100644
index 0000000..c97189e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00042.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00043.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00043.png
new file mode 100644
index 0000000..664c312
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00043.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00044.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00044.png
new file mode 100644
index 0000000..275e75c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00044.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00045.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00045.png
new file mode 100644
index 0000000..4a98b77
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00045.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00046.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00046.png
new file mode 100644
index 0000000..f62b3bc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00046.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00047.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00047.png
new file mode 100644
index 0000000..23915b9
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00047.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00048.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00048.png
new file mode 100644
index 0000000..ad64499
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00048.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00049.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00049.png
new file mode 100644
index 0000000..b259965
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00049.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00050.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00050.png
new file mode 100644
index 0000000..7e17a93
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00050.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00051.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00051.png
new file mode 100644
index 0000000..eaa32b6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00051.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00052.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00052.png
new file mode 100644
index 0000000..c683cb8
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00052.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00053.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00053.png
new file mode 100644
index 0000000..d7eb3fd
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00053.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00054.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00054.png
new file mode 100644
index 0000000..aaeb6db
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00054.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00055.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00055.png
new file mode 100644
index 0000000..8b577e4
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00055.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00056.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00056.png
new file mode 100644
index 0000000..953cd95
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00056.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00057.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00057.png
new file mode 100644
index 0000000..52e5ec3
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00057.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00058.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00058.png
new file mode 100644
index 0000000..681a0f9
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00058.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00059.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00059.png
new file mode 100644
index 0000000..83840b6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00059.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00060.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00060.png
new file mode 100644
index 0000000..8d8eb51
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00060.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00061.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00061.png
new file mode 100644
index 0000000..f4abe38
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00061.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00062.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00062.png
new file mode 100644
index 0000000..d7f9e56
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00062.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00063.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00063.png
new file mode 100644
index 0000000..42d0bd0
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00063.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00064.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00064.png
new file mode 100644
index 0000000..d52b3e6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00064.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00065.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00065.png
new file mode 100644
index 0000000..16c454e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00065.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00066.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00066.png
new file mode 100644
index 0000000..e1c0b1d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00066.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00067.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00067.png
new file mode 100644
index 0000000..15935ff
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00067.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00068.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00068.png
new file mode 100644
index 0000000..981883c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00068.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00069.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00069.png
new file mode 100644
index 0000000..43f1c9f
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00069.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00070.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00070.png
new file mode 100644
index 0000000..addd8c1
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00070.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00071.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00071.png
new file mode 100644
index 0000000..0d964e9
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00071.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00072.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00072.png
new file mode 100644
index 0000000..e1ea8ce
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00072.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00073.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00073.png
new file mode 100644
index 0000000..4314159
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00073.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00074.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00074.png
new file mode 100644
index 0000000..c9c9543
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00074.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00075.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00075.png
new file mode 100644
index 0000000..3db4b49
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00075.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00076.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00076.png
new file mode 100644
index 0000000..cf76b15
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00076.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00077.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00077.png
new file mode 100644
index 0000000..45213e3
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00077.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00078.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00078.png
new file mode 100644
index 0000000..86c56a7
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00078.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00079.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00079.png
new file mode 100644
index 0000000..82c4f55
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00079.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00080.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00080.png
new file mode 100644
index 0000000..d4d8deb
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00080.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00081.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00081.png
new file mode 100644
index 0000000..5e50025
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00081.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00082.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00082.png
new file mode 100644
index 0000000..b840d1d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00082.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00083.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00083.png
new file mode 100644
index 0000000..0398cdc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00083.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00084.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00084.png
new file mode 100644
index 0000000..0d8228e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00084.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00085.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00085.png
new file mode 100644
index 0000000..8496bc6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00085.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00086.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00086.png
new file mode 100644
index 0000000..7885233
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00086.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00087.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00087.png
new file mode 100644
index 0000000..751f94b
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00087.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00088.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00088.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00088.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00089.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00089.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00089.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00090.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00090.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00090.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00091.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00091.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00091.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00092.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00092.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00092.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00093.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00093.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00093.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00094.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00094.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00094.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00095.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00095.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00095.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00096.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00096.png
new file mode 100644
index 0000000..33ad84f
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00096.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00097.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00097.png
new file mode 100644
index 0000000..765ec1a
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00097.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00098.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00098.png
new file mode 100644
index 0000000..9947c94
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00098.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00099.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00099.png
new file mode 100644
index 0000000..db0ac24
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00099.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00100.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00100.png
new file mode 100644
index 0000000..ae93d95
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00100.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00101.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00101.png
new file mode 100644
index 0000000..1b729c0
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00101.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00102.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00102.png
new file mode 100644
index 0000000..22b9527
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00102.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00103.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00103.png
new file mode 100644
index 0000000..8ce8992
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00103.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00104.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00104.png
new file mode 100644
index 0000000..f16dbc2
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00104.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00105.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00105.png
new file mode 100644
index 0000000..5ef6b89
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00105.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00106.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00106.png
new file mode 100644
index 0000000..53ce2bc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00106.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00107.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00107.png
new file mode 100644
index 0000000..fd8ffef
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00107.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00108.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00108.png
new file mode 100644
index 0000000..49b39ed
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00108.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00109.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00109.png
new file mode 100644
index 0000000..aaa55db
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00109.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00110.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00110.png
new file mode 100644
index 0000000..3442634
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00110.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00111.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00111.png
new file mode 100644
index 0000000..7aecef1
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00111.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00112.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00112.png
new file mode 100644
index 0000000..a5278ba
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00112.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00113.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00113.png
new file mode 100644
index 0000000..7e3dc9e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00113.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00114.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00114.png
new file mode 100644
index 0000000..9b93101
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00114.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00115.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00115.png
new file mode 100644
index 0000000..ab90998
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00115.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00116.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00116.png
new file mode 100644
index 0000000..38b3f8f
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00116.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00117.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00117.png
new file mode 100644
index 0000000..5fc2e5c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00117.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00118.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00118.png
new file mode 100644
index 0000000..ccb9eea
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00118.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00119.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00119.png
new file mode 100644
index 0000000..f910866
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00119.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00120.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00120.png
new file mode 100644
index 0000000..ecf8c0c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00120.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00121.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00121.png
new file mode 100644
index 0000000..a0199e5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00121.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00122.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00122.png
new file mode 100644
index 0000000..a697982
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00122.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00123.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00123.png
new file mode 100644
index 0000000..3c88c58
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00123.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00124.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00124.png
new file mode 100644
index 0000000..626e5e5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00124.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00125.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00125.png
new file mode 100644
index 0000000..ae9d3df
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00125.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00126.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00126.png
new file mode 100644
index 0000000..ae5f460
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00126.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00127.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00127.png
new file mode 100644
index 0000000..146759c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00127.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00128.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00128.png
new file mode 100644
index 0000000..064f01d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00128.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00129.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00129.png
new file mode 100644
index 0000000..73d88d5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00129.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00130.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00130.png
new file mode 100644
index 0000000..689df07
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00130.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00131.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00131.png
new file mode 100644
index 0000000..247f995
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00131.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00132.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00132.png
new file mode 100644
index 0000000..f8c5a02
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00132.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00133.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00133.png
new file mode 100644
index 0000000..a0379c7
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00133.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00134.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00134.png
new file mode 100644
index 0000000..90b94ec
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00134.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00135.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00135.png
new file mode 100644
index 0000000..5230de4
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00135.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00136.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00136.png
new file mode 100644
index 0000000..3eb96d5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00136.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00137.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00137.png
new file mode 100644
index 0000000..37774d9
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00137.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00138.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00138.png
new file mode 100644
index 0000000..01c95d0
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00138.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00139.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00139.png
new file mode 100644
index 0000000..f496ce3
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00139.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00140.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00140.png
new file mode 100644
index 0000000..0e13dfb
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00140.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00141.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00141.png
new file mode 100644
index 0000000..46cad3a
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00141.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00142.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00142.png
new file mode 100644
index 0000000..ab40ddc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00142.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00143.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00143.png
new file mode 100644
index 0000000..a70a6cc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00143.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00144.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00144.png
new file mode 100644
index 0000000..e357b73
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00144.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00145.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00145.png
new file mode 100644
index 0000000..813fa51
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00145.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00146.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00146.png
new file mode 100644
index 0000000..469f0a6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00146.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00147.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00147.png
new file mode 100644
index 0000000..9f3f355
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00147.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00148.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00148.png
new file mode 100644
index 0000000..e4f3c2b
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00148.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00149.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00149.png
new file mode 100644
index 0000000..434a379
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00149.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00150.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00150.png
new file mode 100644
index 0000000..12ef618
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00150.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00151.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00151.png
new file mode 100644
index 0000000..a4d2e09
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00151.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00152.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00152.png
new file mode 100644
index 0000000..c657de6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00152.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00153.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00153.png
new file mode 100644
index 0000000..c745f96
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00153.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00154.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00154.png
new file mode 100644
index 0000000..5f389dc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00154.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00155.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00155.png
new file mode 100644
index 0000000..50f51b7
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00155.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00156.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00156.png
new file mode 100644
index 0000000..c905a6c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00156.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00157.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00157.png
new file mode 100644
index 0000000..23f509d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00157.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00158.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00158.png
new file mode 100644
index 0000000..cbd9ebe
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00158.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00159.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00159.png
new file mode 100644
index 0000000..afe61e4
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00159.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00160.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00160.png
new file mode 100644
index 0000000..35eadb5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00160.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00161.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00161.png
new file mode 100644
index 0000000..6d43b14
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00161.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00162.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00162.png
new file mode 100644
index 0000000..f5ac837
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00162.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00163.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00163.png
new file mode 100644
index 0000000..657f59a
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00163.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00164.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00164.png
new file mode 100644
index 0000000..6352deb
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00164.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00165.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00165.png
new file mode 100644
index 0000000..8150c65
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00165.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00166.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00166.png
new file mode 100644
index 0000000..1199a55
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00166.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00167.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00167.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00167.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00168.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00168.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00168.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00169.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00169.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00169.png
Binary files differ
diff --git a/car_product/car_ui_portrait/car_ui_portrait.ini b/car_product/car_ui_portrait/car_ui_portrait.ini
new file mode 100644
index 0000000..7804eca
--- /dev/null
+++ b/car_product/car_ui_portrait/car_ui_portrait.ini
@@ -0,0 +1,11 @@
+hw.audioInput=yes
+hw.lcd.density=160
+hw.gpu.enabled=yes
+hw.camera.back=none
+hw.camera.front=none
+hw.mainKeys=no
+skin.dynamic=yes
+skin.name=1224x2175
+skin.path=1224x2175
+hw.lcd.width=1224
+hw.lcd.height=2175
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/overlay/README b/car_product/car_ui_portrait/overlay/README
new file mode 100644
index 0000000..e49b1b0
--- /dev/null
+++ b/car_product/car_ui_portrait/overlay/README
@@ -0,0 +1,2 @@
+This project currently using the old approach for static RROs targeting the android package due to
+b/186753067. Please use the current approach for RROs for any other application/package.
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp
new file mode 100644
index 0000000..2a4357c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp
@@ -0,0 +1,30 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+    name: "CarEvsCameraPreviewAppRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    certificate: "platform",
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+    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/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.xml
new file mode 100644
index 0000000..5874541
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        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="?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..532ba92
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/config.xml
@@ -0,0 +1,23 @@
+<!--
+  ~ 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>
+
+    <!-- In-plane rotation angle of the rearview camera device in degree -->
+    <integer name="config_evsRearviewCameraInPlaneRotationAngle">180</integer>
+</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..13266b6
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/xml/overlays.xml
@@ -0,0 +1,30 @@
+<?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"/>
+    <item target="integer/config_evsRearviewCameraInPlaneRotationAngle" value="@integer/config_evsRearviewCameraInPlaneRotationAngle"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/Android.bp
new file mode 100644
index 0000000..28a8c0c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/Android.bp
@@ -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.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+    name: "CarUiPortraitCarServiceRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/AndroidManifest.xml
new file mode 100644
index 0000000..3831ee7
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?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.android.car.caruiportrait.rro">
+    <application android:hasCode="false" />
+    <overlay android:priority="20"
+             android:targetPackage="com.android.car"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/values/config.xml b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/values/config.xml
new file mode 100644
index 0000000..43b6e65
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/values/config.xml
@@ -0,0 +1,42 @@
+<?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>
+    <!--
+        Specifies optional features that can be enabled by this image. Note that vhal can disable
+        them depending on product variation.
+        Feature name can be either service name defined in Car.*_SERVICE for Car*Manager or any
+        optional feature defined under @OptionalFeature annotation.
+        Note that '/' is used to have subfeature under main feature like "MAIN_FEATURE/SUB_FEATURE".
+
+        Some examples are:
+        <item>storage_monitoring</item>
+        <item>com.android.car.user.CarUserNoticeService</item>
+        <item>com.example.Feature/SubFeature</item>
+
+        The default list defined below will enable all optional features defined.
+    -->
+    <string-array translatable="false" name="config_allowed_optional_car_features">
+        <item>car_navigation_service</item>
+        <item>cluster_service</item>
+        <item>com.android.car.user.CarUserNoticeService</item>
+        <item>diagnostic</item>
+        <item>storage_monitoring</item>
+        <item>vehicle_map_service</item>
+        <item>car_evs_service</item>
+        <item>car_telemetry_service</item>
+    </string-array>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..61d1bc5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/xml/overlays.xml
@@ -0,0 +1,18 @@
+<!--
+  ~ 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="array/config_allowed_optional_car_features" value="@array/config_allowed_optional_car_features"/>
+</overlay>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/Android.bp
new file mode 100644
index 0000000..92bd722
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/Android.bp
@@ -0,0 +1,31 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+    name: "CarUiPortraitDialerRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+    static_libs: [
+        "androidx-constraintlayout_constraintlayout",
+        "androidx-constraintlayout_constraintlayout-solver",
+        "car-apps-common",
+        "car-ui-lib",
+    ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/AndroidManifest.xml
new file mode 100644
index 0000000..fea65f9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/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.android.car.dialer.caruiportrait.rro">
+    <application android:hasCode="false"/>
+    <overlay android:priority="20"
+             android:targetName="CarDialerApp"
+             android:targetPackage="com.android.car.dialer"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/dialer_ripple_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/dialer_ripple_background.xml
new file mode 100644
index 0000000..131afd5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/dialer_ripple_background.xml
@@ -0,0 +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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+            android:shape="oval">
+            <solid
+                android:color="@color/keypad_background_color" />
+            <size
+                android:width="@dimen/dialer_keypad_button_size"
+                android:height="@dimen/dialer_keypad_button_size"/>
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_arrow_right.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_arrow_right.xml
new file mode 100644
index 0000000..b02823b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_arrow_right.xml
@@ -0,0 +1,26 @@
+<?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="88dp"
+        android:height="88dp"
+        android:viewportWidth="48"
+        android:viewportHeight="48"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="?android:attr/textColorPrimary"
+        android:pathData="M12,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2m0,10c2.7,0 5.8,1.29 6,2L6,18c0.23,-0.72 3.31,-2 6,-2m0,-12C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
+</vector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_backspace.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_backspace.xml
new file mode 100644
index 0000000..4770409
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_backspace.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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="32dp"
+        android:height="32dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM22,19L7.07,19L2.4,12l4.66,-7L22,5v14zM10.41,17L14,13.41 17.59,17 19,15.59 15.41,12 19,8.41 17.59,7 14,10.59 10.41,7 9,8.41 12.59,12 9,15.59z"/>
+</vector>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_bluetooth.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_bluetooth.xml
new file mode 100644
index 0000000..13f130e
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_bluetooth.xml
@@ -0,0 +1,26 @@
+<?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="44dp"
+        android:height="44dp"
+        android:viewportWidth="48"
+        android:viewportHeight="48"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="?android:attr/textColorPrimary"
+        android:pathData="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z"/>
+</vector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_phone.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_phone.xml
new file mode 100644
index 0000000..3e2e824
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_phone.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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="40dp"
+    android:height="40dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:pathData="M0 0h24v24H0z" />
+    <path
+        android:fillColor="?android:attr/textColorPrimary"
+        android:pathData="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27 .67 -.36 1.02-.24 1.12
+.37 2.33 .57 3.57 .57 .55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17
+0-.55 .45 -1 1-1h3.5c.55 0 1 .45 1 1 0 1.25 .2 2.45 .57 3.57 .11 .35 .03 .74-.25
+1.02l-2.2 2.2z" />
+</vector>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/icon_call_button.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/icon_call_button.xml
new file mode 100644
index 0000000..d048af9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/icon_call_button.xml
@@ -0,0 +1,30 @@
+<?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>
+        <shape
+            android:shape="rectangle">
+            <solid
+                android:color="#29cb86" />
+            <corners android:radius="100dp"/>
+            <size
+                android:width="416dp"
+                android:height="@dimen/dialer_keypad_button_size"/>
+        </shape>
+    </item>
+    <item android:drawable="@drawable/ic_phone" android:gravity="center"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/keypad_default_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/keypad_default_background.xml
new file mode 100644
index 0000000..131afd5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/keypad_default_background.xml
@@ -0,0 +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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape
+            android:shape="oval">
+            <solid
+                android:color="@color/keypad_background_color" />
+            <size
+                android:width="@dimen/dialer_keypad_button_size"
+                android:height="@dimen/dialer_keypad_button_size"/>
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/restricted_dialing_mode_label_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/restricted_dialing_mode_label_background.xml
new file mode 100644
index 0000000..ea5a715
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/restricted_dialing_mode_label_background.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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+    <corners android:radius="4dp"/>
+    <solid android:color="@color/car_red_500a"/>
+</shape>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_fragment_with_type_down.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_fragment_with_type_down.xml
new file mode 100644
index 0000000..d192bc9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_fragment_with_type_down.xml
@@ -0,0 +1,94 @@
+<?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.
+  -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginLeft="100dp">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="45dp"
+        android:layout_marginBottom="48dp"
+        android:layout_alignParentTop="true"
+        android:layout_alignLeft="@id/dialpad_fragment"
+        android:layout_alignRight="@id/dialpad_fragment"
+        android:textAppearance="@style/TextAppearance.DialNumber"
+        android:gravity="center"/>
+
+    <com.android.car.ui.recyclerview.CarUiRecyclerView
+        android:id="@+id/list_view"
+        android:layout_width="536dp"
+        android:layout_height="266dp"
+        android:layout_marginBottom="10dp"
+        android:layout_marginLeft="70dp"
+        android:layout_below="@id/title"/>
+    <fragment
+        android:id="@+id/dialpad_fragment"
+        android:name="com.android.car.dialer.ui.dialpad.KeypadFragment"
+        android:layout_height="456dp"
+        android:layout_width="416dp"
+        android:layout_marginLeft="120dp"
+        android:layout_below="@id/list_view"/>
+
+    <RelativeLayout
+        android:layout_height="@dimen/dialer_keypad_button_size"
+        android:layout_width="0dp"
+        android:layout_below="@id/dialpad_fragment"
+        android:layout_marginTop="38dp"
+        android:layout_alignLeft="@id/dialpad_fragment"
+        android:layout_alignRight="@id/dialpad_fragment">
+
+        <ImageView
+            android:id="@+id/call_button"
+            android:layout_height="match_parent"
+            android:layout_width="match_parent"
+            android:adjustViewBounds="true"
+            android:scaleType="fitXY"
+            android:src="@drawable/icon_call_button"
+            android:layout_toLeftOf="@id/delete_button"/>
+
+        <ImageButton
+            android:id="@+id/delete_button"
+            android:layout_width="@dimen/dialer_keypad_button_size"
+            android:layout_height="@dimen/dialer_keypad_button_size"
+            style="@style/DialpadSecondaryButton"
+            android:src="@drawable/ic_backspace"
+            android:layout_marginLeft="64dp"
+            android:visibility="gone"
+            android:layout_alignParentRight="true"/>
+    </RelativeLayout>
+
+    <include
+        layout="@layout/dialpad_user_profile"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginTop="10dp"
+        android:layout_below="@id/title"
+        android:layout_centerHorizontal="true"/>
+
+    <include
+        layout="@layout/restricted_dialing_mode_label"
+        android:id="@+id/restricted_dialing_mode_label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:layout_marginTop="8dp"
+        android:layout_below="@id/title"
+        android:visibility="invisible"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_user_profile.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_user_profile.xml
new file mode 100644
index 0000000..bb4e11f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_user_profile.xml
@@ -0,0 +1,54 @@
+<?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.
+  -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/display_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.DialpadDisplayName"
+        android:singleLine="true"
+        android:layout_centerHorizontal="true"
+        android:layout_alignParentTop="true"/>
+
+    <TextView
+        android:id="@+id/label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:singleLine="true"
+        android:layout_marginTop="12dp"
+        android:layout_below="@id/display_name"
+        android:layout_centerHorizontal="true"/>
+
+    <ImageView
+        android:id="@+id/dialpad_contact_avatar"
+        android:layout_height="@dimen/dialpad_contact_avatar_size"
+        android:layout_width="@dimen/dialpad_contact_avatar_size"
+        android:layout_below="@id/label"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"/>
+
+    <TextView
+        android:id="@+id/dialpad_contact_initials"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:layout_width="10dp"
+        android:layout_height="10dp"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/no_hfp.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/no_hfp.xml
new file mode 100644
index 0000000..881d0d5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/no_hfp.xml
@@ -0,0 +1,67 @@
+<?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">
+
+        <RelativeLayout
+            android:id="@+id/no_hfp_error_container"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <Button
+                android:id="@+id/emergency_call_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/emergency_button_text"
+                android:minWidth="156dp"
+                android:minHeight="76dp"
+                android:background="?android:attr/selectableItemBackground"
+                android:textColor="#ffd50000"
+                android:layout_marginBottom="@dimen/car_ui_padding_4"
+                android:layout_alignParentBottom="true"
+                android:layout_centerHorizontal="true"/>
+
+            <ImageView
+                android:id="@+id/error_icon"
+                android:layout_width="96dp"
+                android:layout_height="96dp"
+                android:src="@drawable/ic_bluetooth"
+                android:layout_marginBottom="@dimen/car_ui_padding_3"
+                android:layout_alignLeft="@id/error_string"
+                android:layout_alignRight="@id/error_string"
+                android:layout_above="@id/error_string"
+                android:gravity="center"/>
+
+            <TextView
+                android:id="@+id/error_string"
+                style="@style/FullScreenErrorMessageStyle"
+                android:text="@string/no_hfp"
+                android:layout_centerInParent="true"/>
+
+            <com.android.car.apps.common.UxrButton
+                android:id="@+id/connect_bluetooth_button"
+                style="@style/FullScreenErrorButtonStyle"
+                android:background="@color/keypad_background_color"
+                android:text="@string/connect_bluetooth_button_text"
+                android:layout_marginTop="@dimen/car_ui_padding_5"
+                android:layout_below="@id/error_string"
+                android:layout_centerHorizontal="true"/>
+        </RelativeLayout>
+
+</FrameLayout>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/restricted_dialing_mode_label.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/restricted_dialing_mode_label.xml
new file mode 100644
index 0000000..6a23fd5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/restricted_dialing_mode_label.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ 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.
+  -->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/restricted_dialing_mode_label"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="8dp"
+    android:padding="8dp"
+    android:textAppearance="@style/TextAppearance.Body2"
+    android:text="@string/restricted_dialing_mode_label"
+    android:alpha="0.8"
+    android:background="@drawable/restricted_dialing_mode_label_background"/>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/type_down_list_item.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/type_down_list_item.xml
new file mode 100644
index 0000000..68cf567
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/type_down_list_item.xml
@@ -0,0 +1,55 @@
+<?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.
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contact_result"
+    android:foreground="?android:attr/selectableItemBackground"
+    android:layout_width="match_parent"
+    android:layout_height="112dp">
+
+    <ImageView
+        android:id="@+id/contact_picture"
+        android:layout_width="72dp"
+        android:layout_height="72dp"
+        android:scaleType="centerCrop"
+        android:layout_centerVertical="true"
+        android:layout_alignParentLeft="true"/>
+
+    <TextView
+        android:id="@+id/contact_name"
+        android:layout_marginStart="24dp"
+        android:layout_marginTop="14dp"
+        android:layout_marginBottom="8dp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:textAppearance="@style/TextAppearance.ContactResultTitle"
+        android:duplicateParentState="true"
+        android:layout_alignParentTop="true"
+        android:layout_toRightOf="@id/contact_picture"/>
+
+    <TextView
+        android:id="@+id/phone_number"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="14dp"
+        android:theme="@style/Theme.CarUi.WithToolbar"
+        android:singleLine="true"
+        android:layout_alignParentBottom="true"
+        android:layout_alignLeft="@id/contact_name"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..937c35b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values-night/colors.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:android="http://schemas.android.com/apk/res/android">
+    <color name="keypad_background_color">#282A2D</color>
+    <color name="divider_color">#2e3134</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/colors.xml
new file mode 100644
index 0000000..9b5d97c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/colors.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:android="http://schemas.android.com/apk/res/android">
+    <color name="keypad_background_color">#E8EAED</color>
+    <color name="divider_color">#E8EAED</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/configs.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/configs.xml
new file mode 100644
index 0000000..2f28c76
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/configs.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>
+    <integer name="config_dialed_number_gravity">1</integer>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/dimens.xml
new file mode 100644
index 0000000..108cfb1
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/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>
+    <dimen name="dialer_keypad_button_size">96dp</dimen>
+    <dimen name="dialpad_contact_avatar_size">64dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/strings.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/strings.xml
new file mode 100644
index 0000000..5f273dc
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?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">
+    <string name="restricted_dialing_mode_label">Dialpad usage is restricted while driving</string>
+    <string name="emergency_button_text">Emergency</string>
+    <string name="connect_bluetooth_button_text">Connect to Bluetooth</string>
+    <string name="no_hfp">
+        To complete your call, first connect your phone to your car via Bluetooth.
+    </string>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/styles.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/styles.xml
new file mode 100644
index 0000000..d67d485
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/styles.xml
@@ -0,0 +1,72 @@
+<?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">
+    <!-- Phone -->
+    <style name="KeypadButtonStyle">
+        <item name="android:clickable">true</item>
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginTop">12dp</item>
+        <item name="android:layout_marginRight">32dp</item>
+        <item name="android:layout_marginBottom">12dp</item>
+        <item name="android:layout_marginLeft">32dp</item>
+        <item name="android:background">@drawable/keypad_default_background</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.DialNumber" parent="android:style/TextAppearance">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textSize">32sp</item>
+    </style>
+
+    <style name="SubheaderText" parent="android:style/TextAppearance">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textSize">24sp</item>
+        <item name="android:textFontWeight">500</item>
+        <item name="android:textStyle">normal</item>
+    </style>
+
+    <style name="AddFavoriteText">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
+    <!-- Call history -->
+    <style name="TextAppearance.CallLogTitleDefault" parent="TextAppearance.Body1">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+    <!-- Customized text color for missed calls can be added here -->
+    <style name="TextAppearance.CallLogTitleMissedCall" parent="TextAppearance.Body1">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
+    <style name="DialpadSecondaryButton">
+        <item name="android:background">@drawable/dialer_ripple_background</item>
+        <item name="android:scaleType">centerInside</item>
+        <item name="android:tint">?android:attr/textColorPrimary</item>
+    </style>
+
+    <style name="TextAppearance.DialpadDisplayName" parent="TextAppearance.Body1"/>
+
+    <style name="TextAppearance.ContactResultTitle" parent="TextAppearance.Body1">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
+    <style name="TextAppearance.TypeDownListSpan" parent="TextAppearance.Body3">
+        <item name="android:textSize">32sp</item>
+        <item name="android:textColor">#29cb86</item>
+    </style>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..848c5eb
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/xml/overlays.xml
@@ -0,0 +1,66 @@
+<?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="drawable/ic_phone" value="@drawable/ic_phone"/>
+    <item target="drawable/icon_call_button" value="@drawable/icon_call_button"/>
+    <item target="drawable/ic_backspace" value="@drawable/ic_backspace"/>
+    <item target="drawable/dialer_ripple_background" value="@drawable/dialer_ripple_background"/>
+    <item target="drawable/restricted_dialing_mode_label_background" value="@drawable/restricted_dialing_mode_label_background"/>
+    <item target="drawable/ic_arrow_right" value="@drawable/ic_arrow_right"/>
+    <item target="drawable/ic_bluetooth" value="@drawable/ic_bluetooth"/>
+
+    <item target="id/dialpad_fragment" value="@id/dialpad_fragment" />
+    <item target="id/call_button" value="@id/call_button" />
+    <item target="id/title" value="@id/title" />
+    <item target="id/delete_button" value="@id/delete_button" />
+    <item target="id/display_name" value="@id/display_name" />
+    <item target="id/label" value="@id/label" />
+    <item target="id/dialpad_contact_avatar" value="@id/dialpad_contact_avatar" />
+    <item target="id/dialpad_contact_initials" value="@id/dialpad_contact_initials" />
+    <item target="id/list_view" value="@id/list_view" />
+    <item target="id/restricted_dialing_mode_label" value="@id/restricted_dialing_mode_label" />
+    <item target="id/contact_picture" value="@id/contact_picture" />
+    <item target="id/contact_result" value="@id/contact_result" />
+    <item target="id/contact_name" value="@id/contact_name" />
+    <item target="id/phone_number" value="@id/phone_number" />
+    <item target="id/no_hfp_error_container" value="@id/no_hfp_error_container" />
+    <item target="id/emergency_call_button" value="@id/emergency_call_button" />
+    <item target="id/error_icon" value="@id/error_icon" />
+    <item target="id/error_string" value="@id/error_string" />
+    <item target="id/connect_bluetooth_button" value="@id/connect_bluetooth_button" />
+
+    <item target="color/divider_color" value="@color/divider_color" />
+    <item target="color/hero_button_background_color" value="@color/hero_button_background_color" />
+
+    <item target="integer/config_dialed_number_gravity" value="@integer/config_dialed_number_gravity" />
+
+    <item target="layout/dialpad_fragment_with_type_down" value="@layout/dialpad_fragment_with_type_down"/>
+    <item target="layout/dialpad_user_profile" value="@layout/dialpad_user_profile"/>
+    <item target="layout/type_down_list_item" value="@layout/type_down_list_item"/>
+    <item target="layout/restricted_dialing_mode_label" value="@layout/restricted_dialing_mode_label"/>
+    <item target="layout/no_hfp" value="@layout/no_hfp"/>
+
+    <item target="style/KeypadButtonStyle" value="@style/KeypadButtonStyle"/>
+    <item target="style/TextAppearance.DialNumber" value="@style/TextAppearance.DialNumber"/>
+    <item target="style/SubheaderText" value="@style/SubheaderText"/>
+    <item target="style/AddFavoriteText" value="@style/AddFavoriteText"/>
+    <item target="style/TextAppearance.CallLogTitleDefault" value="@style/TextAppearance.CallLogTitleDefault"/>
+    <item target="style/TextAppearance.DialpadDisplayName" value="@style/TextAppearance.DialpadDisplayName"/>
+    <item target="style/TextAppearance.ContactResultTitle" value="@style/TextAppearance.ContactResultTitle"/>
+    <item target="style/TextAppearance.TypeDownListSpan" value="@style/TextAppearance.TypeDownListSpan"/>
+
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp
new file mode 100644
index 0000000..7e3df0a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp
@@ -0,0 +1,31 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+    name: "CarUiPortraitLauncherRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+    static_libs: [
+        "androidx.cardview_cardview",
+        "androidx-constraintlayout_constraintlayout",
+        "car-media-common",
+        "car-apps-common",
+    ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/AndroidManifest.xml
new file mode 100644
index 0000000..1969da8
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/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.android.car.carlauncher.caruiportrait.rro">
+    <application android:hasCode="false"/>
+    <overlay android:priority="20"
+             android:targetName="CarLauncher"
+             android:targetPackage="com.android.car.carlauncher"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/control_bar_image_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/control_bar_image_background.xml
new file mode 100644
index 0000000..9a3c589
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/control_bar_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/control_bar_image_background_radius"/>
+</shape>
\ No newline at end of file
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..c1fd0e9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/default_audio_background.xml
@@ -0,0 +1,39 @@
+<?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>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/default_audio_background_color"/>
+        </shape>
+    </item>
+    <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-land/car_launcher.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout-land/car_launcher.xml
new file mode 100644
index 0000000..7f79ef1
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout-land/car_launcher.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal"
+    android:layoutDirection="ltr">
+
+    <com.android.car.ui.FocusArea
+        android:id="@+id/bottom_card"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:padding="10dp"
+        android:layoutDirection="locale"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_only.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_only.xml
new file mode 100644
index 0000000..fbd91ba
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_only.xml
@@ -0,0 +1,38 @@
+<?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.
+  -->
+
+<!-- Layout for a DescriptiveTextView. Required by HomeCardFragment, but currently not used by the CarUiPortrait launcher. -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:orientation="horizontal">
+
+    <include layout="@layout/descriptive_text"
+             android:layout_height="match_parent"
+             android:layout_width="wrap_content"/>
+
+    <TextView
+        android:id="@+id/tap_for_more_text"
+        android:layout_height="match_parent"
+        android:layout_width="wrap_content"
+        android:singleLine="true"
+        android:text="Tap for more"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:visibility="gone"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_with_controls.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_with_controls.xml
new file mode 100644
index 0000000..90a9042
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_with_controls.xml
@@ -0,0 +1,70 @@
+<?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.
+  -->
+
+<!-- Layout for a DescriptiveTextWithControlsView. Required by HomeCardFragment, but currently not used by the CarUiPortrait launcher. -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent">
+
+    <include layout="@layout/descriptive_text"
+             android:layout_height="match_parent"
+             android:layout_width="0dp"
+             android:layout_weight="1"
+             android:layout_gravity="start|center_vertical"/>
+
+    <LinearLayout
+        android:id="@+id/button_trio"
+        android:gravity="center"
+        android:layout_height="match_parent"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:orientation="horizontal"
+        android:layout_gravity="end|center_vertical">
+
+        <ImageButton
+            android:id="@+id/button_left"
+            android:layout_height="@dimen/control_bar_action_icon_size"
+            android:layout_width="@dimen/control_bar_action_icon_size"
+            android:background="@android:color/transparent"
+            android:scaleType="centerInside"/>
+
+        <Space
+            android:layout_height="match_parent"
+            android:layout_width="0dp"
+            android:layout_weight="1"/>
+
+        <ImageButton
+            android:id="@+id/button_center"
+            android:layout_height="@dimen/control_bar_action_icon_size"
+            android:layout_width="@dimen/control_bar_action_icon_size"
+            android:background="@android:color/transparent"
+            android:scaleType="centerInside"/>
+
+        <Space
+            android:layout_height="match_parent"
+            android:layout_width="0dp"
+            android:layout_weight="1"/>
+
+        <ImageButton
+            android:id="@+id/button_right"
+            android:layout_height="@dimen/control_bar_action_icon_size"
+            android:layout_width="@dimen/control_bar_action_icon_size"
+            android:background="@android:color/transparent"
+            android:scaleType="centerInside"/>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
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
new file mode 100644
index 0000000..932a22f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_media.xml
@@ -0,0 +1,46 @@
+<?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.
+  -->
+
+<!-- Layout specifically for the media card, which uses media-specific playback_controls.xml -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:orientation="horizontal">
+
+    <include layout="@layout/descriptive_text"
+             android:id="@+id/media_descriptive_text"
+             android:layout_height="match_parent"
+             android:layout_width="0dp"
+             android:layout_weight="1"
+             android:layout_gravity="start"/>
+
+    <FrameLayout
+        android:layout_height="match_parent"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        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: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_content_text_block.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_text_block.xml
new file mode 100644
index 0000000..f08886b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_text_block.xml
@@ -0,0 +1,41 @@
+<?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.
+  -->
+
+<!-- Layout for a TextBlockView. Required by HomeCardFragment, but currently not used by the CarUiPortrait launcher. -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:orientation="horizontal">
+
+    <TextView
+        android:id="@+id/text_block"
+        android:gravity="start"
+        android:layout_height="match_parent"
+        android:layout_width="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+    <TextView
+        android:id="@+id/tap_for_more_text"
+        android:layout_height="match_parent"
+        android:layout_width="wrap_content"
+        android:singleLine="true"
+        android:text="Tap for more"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:visibility="gone"/>
+
+</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
new file mode 100644
index 0000000..5078a1b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_fragment.xml
@@ -0,0 +1,108 @@
+<?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.cardview.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/card_view"
+    android:background="?android:attr/colorBackgroundFloating"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:visibility="gone">
+
+    <FrameLayout
+        android:id="@+id/card_background"
+        android:visibility="gone"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+    <RelativeLayout
+        android:layout_height="match_parent"
+        android:layout_width="match_parent">
+
+        <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_centerVertical="true"
+            android:layout_marginStart="@dimen/card_icon_margin_start">
+
+            <com.android.car.apps.common.CrossfadeImageView
+                android:id="@+id/card_background_image"
+                android:background="@drawable/control_bar_image_background"
+                android:clipToOutline="true"
+                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"
+            android:layout_width="0dp"
+            android:visibility="gone"
+            android:layout_centerVertical="true"
+            android:layout_toEndOf="@id/card_icon"/>
+
+        <FrameLayout
+            android:layout_height="match_parent"
+            android:layout_width="0dp"
+            android:layout_toEndOf="@id/control_bar_image_container"
+            android:layout_alignParentEnd="true"
+            android:layout_marginStart="@dimen/card_content_margin_start">
+
+            <ViewStub android:id="@+id/media_layout"
+                      android:inflatedId="@+id/media_layout"
+                      android:layout_height="match_parent"
+                      android:layout_width="match_parent"
+                      android:visibility="gone"
+                      android:layout="@layout/card_content_media"/>
+
+            <!-- Following ViewStubs are required by the HomeCardFragment, but are currently unused
+            as the portrait launcher only shows an audio card and the respective media layout. -->
+            <ViewStub android:id="@+id/descriptive_text_layout"
+                      android:inflatedId="@+id/descriptive_text_layout"
+                      android:layout_height="match_parent"
+                      android:layout_width="match_parent"
+                      android:visibility="gone"
+                      android:layout="@layout/card_content_descriptive_text_only"/>
+
+            <ViewStub android:id="@+id/text_block_layout"
+                      android:inflatedId="@+id/text_block_layout"
+                      android:layout_height="match_parent"
+                      android:layout_width="match_parent"
+                      android:visibility="gone"
+                      android:layout="@layout/card_content_text_block"/>
+
+            <ViewStub android:id="@+id/descriptive_text_with_controls_layout"
+                      android:inflatedId="@+id/descriptive_text_with_controls_layout"
+                      android:layout_height="match_parent"
+                      android:layout_width="match_parent"
+                      android:visibility="gone"
+                      android:layout="@layout/card_content_descriptive_text_with_controls"/>
+
+        </FrameLayout>
+    </RelativeLayout>
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/descriptive_text.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/descriptive_text.xml
new file mode 100644
index 0000000..1c55cca
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/descriptive_text.xml
@@ -0,0 +1,73 @@
+<?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.
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent">
+
+    <!-- optional_image is required by the HomeCardFragment. Intentionally not shown by setting
+    0 height and width. -->
+    <ImageView
+        android:id="@+id/optional_image"
+        android:layout_height="0dp"
+        android:layout_width="0dp"
+        android:visibility="gone"
+        android:layout_alignParentStart="true"/>
+
+    <TextView
+        android:id="@+id/primary_text"
+        android:layout_height="wrap_content"
+        android:layout_width="0dp"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentEnd="true"
+        android:layout_alignParentTop="true"
+        android:layout_marginTop="@dimen/descriptive_text_vertical_margin"/>
+
+    <Chronometer
+        android:id="@+id/optional_timer"
+        android:visibility="gone"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentBottom="true"
+        android:layout_marginBottom="@dimen/descriptive_text_vertical_margin"/>
+
+    <TextView
+        android:id="@+id/optional_timer_separator"
+        android:visibility="gone"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:text="@string/ongoing_call_duration_text_separator"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:layout_toEndOf="@id/optional_timer"
+        android:layout_alignParentBottom="true"
+        android:layout_marginBottom="@dimen/descriptive_text_vertical_margin"/>
+
+    <TextView
+        android:id="@+id/secondary_text"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:singleLine="true"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:layout_toEndOf="@id/optional_timer_separator"
+        android:layout_alignParentBottom="true"
+        android:layout_marginBottom="@dimen/descriptive_text_vertical_margin"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/title_bar_display_area_view.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/title_bar_display_area_view.xml
new file mode 100644
index 0000000..940e413
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/title_bar_display_area_view.xml
@@ -0,0 +1,38 @@
+<?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.
+  -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/title_bar"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/title_bar_display_area_height"
+    android:background="@color/title_bar_display_area_background_color">
+    <View
+        android:layout_width="120dp"
+        android:layout_height="6dp"
+        android:background="@color/title_bar_display_area_handle_bar_color"
+        android:layout_marginTop="17dp"
+        android:layout_centerInParent="true" />
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"/>
+</RelativeLayout>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..59ed06b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values-night/colors.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.
+  -->
+
+<resources>
+    <color name="default_audio_background_image_color">#515355</color>
+    <color name="default_audio_background_color">#1E2125</color>
+    <color name="title_bar_display_area_handle_bar_color">#282a2d</color>
+    <color name="title_bar_display_area_background_color">#000000</color>
+
+    <color name="icon_tint">#e8eaed</color>
+</resources>
\ No newline at end of file
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
new file mode 100644
index 0000000..266da92
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/colors.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.
+  -->
+
+<resources>
+    <color name="default_audio_background_image_color">#DADCE0</color>
+    <color name="default_audio_background_color">#BDC1C6</color>
+    <color name="title_bar_display_area_handle_bar_color">#e8eaed</color>
+    <color name="title_bar_display_area_background_color">#ffffff</color>
+
+    <color name="icon_tint">#000000</color>
+    <color name="media_button_tint">@color/icon_tint</color>
+    <color name="dialer_button_icon_color">@color/icon_tint</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
new file mode 100644
index 0000000..c912ab9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/config.xml
@@ -0,0 +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.
+  -->
+
+<resources>
+    <!-- A list of package names that provide the cards to display on the home screen -->
+    <string-array name="config_homeCardModuleClasses" translatable="false">
+        <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
new file mode 100644
index 0000000..9dcb0e9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/dimens.xml
@@ -0,0 +1,55 @@
+<?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>
+    <dimen name="card_icon_margin_start">8dp</dimen>
+    <dimen name="card_content_margin_start">16dp</dimen>
+
+    <dimen name="descriptive_text_vertical_margin">23dp</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">88dp</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 -->
+    <dimen name="total_screen_height">2175dp</dimen>
+    <!-- screen width of the device. This is used when custom policy is provided using
+    config_deviceSpecificDisplayAreaPolicyProvider -->
+    <dimen name="total_screen_width">1224dp</dimen>
+
+    <dimen name="control_bar_height">136dp</dimen>
+    <dimen name="control_bar_padding">0dp</dimen>
+    <!-- This height is from the top of navbar not accounting for the control bar height. -->
+    <dimen name="default_app_display_area_height">1055dp</dimen>
+    <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>
+
+    <dimen name="control_bar_image_background_radius">24dp</dimen>
+
+    <dimen name="button_tap_target_size">88dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/integers.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/integers.xml
new file mode 100644
index 0000000..7d54116
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/integers.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>
+    <!-- Number of buttons shown for the media playback controls bar -->
+    <integer name="playback_controls_bar_columns">3</integer>
+    <!--  Entry/exit animation transition speed in milliseconds.  This is the animation of foreground DA.-->
+    <integer name="enter_exit_animation_foreground_display_area_duration_ms">500</integer>
+</resources>
+
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/strings.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/strings.xml
new file mode 100644
index 0000000..2c0a471
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/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="ongoing_call_duration_text_separator">&#160;&#8226;&#160;</string>
+</resources>
\ No newline at end of file
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
new file mode 100644
index 0000000..44c23f8
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/xml/overlays.xml
@@ -0,0 +1,67 @@
+<!--
+  ~ 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="color/dialer_button_icon_color" value="@color/dialer_button_icon_color"/>
+    <item target="color/icon_tint" value="@color/icon_tint" />
+    <item target="color/media_button_tint" value="@color/media_button_tint"/>
+
+    <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_icon" value="@id/card_icon" />
+    <item target="id/card_name" value="@id/card_name" />
+    <item target="id/card_view" value="@id/card_view" />
+    <item target="id/descriptive_text_layout" value="@id/descriptive_text_layout" />
+    <item target="id/text_block_layout" value="@id/text_block_layout" />
+    <item target="id/descriptive_text_with_controls_layout" value="@id/descriptive_text_with_controls_layout" />
+    <item target="id/media_descriptive_text" value="@id/media_descriptive_text" />
+    <item target="id/media_layout" value="@id/media_layout"/>
+    <item target="id/media_playback_controls_bar" value="@id/media_playback_controls_bar" />
+    <item target="id/optional_image" value="@id/optional_image" />
+    <item target="id/optional_timer" value="@id/optional_timer"/>
+    <item target="id/optional_timer_separator" value="@id/optional_timer_separator"/>
+    <item target="id/primary_text" value="@id/primary_text" />
+    <item target="id/secondary_text" value="@id/secondary_text"/>
+    <item target="id/title" value="@id/title"/>
+
+    <item target="id/button_trio" value="@id/button_trio"/>
+    <item target="id/button_center" value="@id/button_center"/>
+    <item target="id/button_left" value="@id/button_left"/>
+    <item target="id/button_right" value="@id/button_right"/>
+
+    <item target="integer/enter_exit_animation_foreground_display_area_duration_ms" value="@integer/enter_exit_animation_foreground_display_area_duration_ms"/>
+
+    <item target="layout/card_content_media" value="@layout/card_content_media" />
+    <item target="layout/card_fragment" value="@layout/card_fragment" />
+    <item target="layout/car_launcher" value="@layout/car_launcher"/>
+    <item target="layout/descriptive_text" value="@layout/descriptive_text" />
+    <item target="layout/title_bar_display_area_view" value="@layout/title_bar_display_area_view" />
+
+    <item target="dimen/card_background_image_blur_radius" value="@dimen/card_background_image_blur_radius" />
+    <item target="dimen/control_bar_height" value="@dimen/control_bar_height"/>
+    <item target="dimen/control_bar_padding" value="@dimen/control_bar_padding"/>
+    <item target="dimen/default_app_display_area_height" value="@dimen/default_app_display_area_height"/>
+    <item target="dimen/button_tap_target_size" value="@dimen/button_tap_target_size"/>
+    <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="dimen/total_screen_height" value="@dimen/total_screen_height"/>
+    <item target="dimen/total_screen_width" value="@dimen/total_screen_width"/>
+
+    <item target="drawable/default_audio_background" value="@drawable/default_audio_background"/>
+
+    <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/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/Android.bp
new file mode 100644
index 0000000..a6beaa0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/Android.bp
@@ -0,0 +1,30 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+    name: "CarUiPortraitMediaRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+    static_libs: [
+        "androidx-constraintlayout_constraintlayout",
+        "car-apps-common",
+
+    ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/AndroidManifest.xml
new file mode 100644
index 0000000..fa12e4f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/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.android.car.media.caruiportrait.rro">
+    <application android:hasCode="false"/>
+    <overlay android:priority="20"
+             android:targetName="CarMediaApp"
+             android:targetPackage="com.android.car.media"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true" />
+</manifest>
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/layout/fragment_error.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/layout/fragment_error.xml
new file mode 100644
index 0000000..dc03e18
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/layout/fragment_error.xml
@@ -0,0 +1,88 @@
+<?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.
+  -->
+<RelativeLayout
+    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">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="0dp"
+        android:layout_height="0dp">
+        <Space
+            android:id="@+id/ui_content_start_guideline"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_marginLeft="0dp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+        />
+
+        <Space
+            android:id="@+id/ui_content_top_guideline"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginTop="96dp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+        />
+
+        <Space
+            android:id="@+id/ui_content_end_guideline"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_marginRight="0dp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+        />
+
+        <Space
+            android:id="@+id/ui_content_bottom_guideline"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginBottom="0dp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+        />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <com.android.car.apps.common.UxrTextView
+        android:id="@+id/error_message"
+        android:layout_width="520dp"
+        android:layout_height="44dp"
+        android:gravity="center"
+        android:layout_marginTop="440dp"
+        android:layout_alignParentTop="true"
+        android:layout_centerHorizontal="true"/>
+
+    <com.android.car.apps.common.UxrButton
+        android:id="@+id/error_button"
+        android:layout_width="760dp"
+        android:layout_height="88dp"
+        android:background="@color/button_background_color"
+        android:textAlignment="center"
+        android:gravity="center"
+        android:layout_marginTop="120dp"
+        android:layout_centerHorizontal="true"
+        android:layout_below="@id/error_message"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..83db0e3
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values-night/colors.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 xmlns:android="http://schemas.android.com/apk/res/android">
+    <color name="button_background_color">#282A2D</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/bools.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/bools.xml
new file mode 100644
index 0000000..8f979df
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/bools.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.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Now playing and mini playback controls will be shown in sysui instead of media center. -->
+    <bool name="show_mini_playback_controls">false</bool>
+    <bool name="switch_to_playback_view_when_playable_item_is_clicked">false</bool>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/colors.xml
new file mode 100644
index 0000000..14bc52b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/colors.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 xmlns:android="http://schemas.android.com/apk/res/android">
+    <color name="button_background_color">#E8EAED</color>
+</resources>
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
new file mode 100644
index 0000000..01f2039
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/xml/overlays.xml
@@ -0,0 +1,47 @@
+<!--
+  ~ 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="attr/layout_constraintBottom_toBottomOf" value="@attr/layout_constraintBottom_toBottomOf"/>
+    <item target="attr/layout_constraintBottom_toTopOf" value="@attr/layout_constraintBottom_toTopOf"/>
+    <item target="attr/layout_constraintEnd_toEndOf" value="@attr/layout_constraintEnd_toEndOf"/>
+    <item target="attr/layout_constraintEnd_toStartOf" value="@attr/layout_constraintEnd_toStartOf"/>
+    <item target="attr/layout_constraintGuide_begin" value="@attr/layout_constraintGuide_begin"/>
+    <item target="attr/layout_constraintGuide_end" value="@attr/layout_constraintGuide_end"/>
+    <item target="attr/layout_constraintHorizontal_bias" value="@attr/layout_constraintHorizontal_bias"/>
+    <item target="attr/layout_constraintLeft_toLeftOf" value="@attr/layout_constraintLeft_toLeftOf"/>
+    <item target="attr/layout_constraintLeft_toRightOf" value="@attr/layout_constraintLeft_toRightOf"/>
+    <item target="attr/layout_constraintRight_toLeftOf" value="@attr/layout_constraintRight_toLeftOf"/>
+    <item target="attr/layout_constraintRight_toRightOf" value="@attr/layout_constraintRight_toRightOf"/>
+    <item target="attr/layout_constraintStart_toEndOf" value="@attr/layout_constraintStart_toEndOf"/>
+    <item target="attr/layout_constraintStart_toStartOf" value="@attr/layout_constraintStart_toStartOf"/>
+    <item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf"/>
+    <item target="attr/layout_constraintTop_toTopOf" value="@attr/layout_constraintTop_toTopOf"/>
+
+    <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="id/error_message" value="@id/error_message" />
+    <item target="id/error_button" value="@id/error_button" />
+    <item target="id/ui_content_start_guideline" value="@id/ui_content_start_guideline" />
+    <item target="id/ui_content_top_guideline" value="@id/ui_content_top_guideline" />
+    <item target="id/ui_content_end_guideline" value="@id/ui_content_end_guideline" />
+    <item target="id/ui_content_bottom_guideline" value="@id/ui_content_bottom_guideline" />
+
+    <item target="layout/fragment_error" value="@layout/fragment_error"/>
+
+    <item target="style/MediaIconContainerStyle" value="@style/MediaIconContainerStyle" />
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/Android.bp
new file mode 100644
index 0000000..960301a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/Android.bp
@@ -0,0 +1,28 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+    name: "CarUiPortraitNotificationRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+    static_libs: [
+        "CarNotificationLib",
+    ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/AndroidManifest.xml
new file mode 100644
index 0000000..47e6a4a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?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.android.car.notification.caruiportrait.rro">
+    <application android:hasCode="false"/>
+    <overlay android:priority="20"
+             android:targetName="CarNotification"
+             android:targetPackage="com.android.car.notification"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/call_headsup_notification_template.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/call_headsup_notification_template.xml
new file mode 100644
index 0000000..41095c4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/call_headsup_notification_template.xml
@@ -0,0 +1,74 @@
+<?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.
+  -->
+
+<com.android.car.ui.FocusArea
+    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="wrap_content">
+
+    <androidx.cardview.widget.CardView
+        android:id="@+id/card_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        app:cardCornerRadius="@dimen/notification_card_radius">
+
+        <RelativeLayout
+            android:id="@+id/inner_template_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/car_notification_card_inner_top_margin">
+
+            <com.android.car.notification.template.CarNotificationHeaderView
+                android:id="@+id/notification_header"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_alignParentStart="true"
+                android:layout_alignParentTop="true"
+                app:isHeadsUp="true"/>
+
+            <com.android.car.notification.template.CarNotificationBodyView
+                android:id="@+id/notification_body"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="@dimen/notification_touch_target_size"
+                android:gravity="center_vertical"
+                android:layout_alignParentTop="true"
+                android:layout_alignParentStart="true"
+                android:layout_alignParentEnd="true"
+                android:layout_marginStart="@dimen/card_body_margin_start"
+                app:maxLines="@integer/config_headsUpNotificationMaxBodyLines"
+                app:showBigIcon="true"
+                app:isHeadsUp="true"/>
+
+            <FrameLayout
+                android:id="@+id/notification_actions_wrapper"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_below="@id/notification_body">
+
+                <com.android.car.notification.template.CarNotificationActionsView
+                    android:id="@+id/notification_actions"
+                    style="@style/NotificationActionViewLayout"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    app:categoryCall="true"/>
+            </FrameLayout>
+        </RelativeLayout>
+    </androidx.cardview.widget.CardView>
+</com.android.car.ui.FocusArea>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_body_view.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_body_view.xml
new file mode 100644
index 0000000..a373e8a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_body_view.xml
@@ -0,0 +1,50 @@
+<?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.
+  -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <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/body_big_icon_margin"
+        style="@style/NotificationBodyImageIcon"/>
+
+    <TextView
+        android:id="@+id/notification_body_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_toEndOf="@id/notification_body_icon"
+        android:layout_alignTop="@id/notification_body_icon"
+        android:layout_marginStart="@dimen/card_start_margin"
+        android:layout_alignWithParentIfMissing="true"
+        android:layout_marginTop="8dp"
+        style="@style/NotificationBodyTitleText"/>
+
+    <TextView
+        android:id="@+id/notification_body_content"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_toEndOf="@id/notification_body_icon"
+        android:layout_below="@id/notification_body_title"
+        android:layout_marginStart="@dimen/card_start_margin"
+        android:layout_marginTop="4dp"
+        android:layout_alignWithParentIfMissing="true"
+        style="@style/NotificationBodyContentText"/>
+</merge>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_header_view.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_header_view.xml
new file mode 100644
index 0000000..7a77a81
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_header_view.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.
+  -->
+
+<!-- Do not show app icon or name for HUNs -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <ImageView
+        android:id="@+id/app_icon"
+        android:layout_width="0dp"
+        android:layout_height="0dp"/>
+
+    <TextView
+        android:id="@+id/header_text"
+        android:layout_width="0dp"
+        android:layout_height="0dp"/>
+</merge>
\ No newline at end of file
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
new file mode 100644
index 0000000..a3c2ce3
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_notification_actions_view.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.
+  -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <com.android.car.notification.template.CarNotificationActionButton
+            android:id="@+id/action_1"
+            style="@style/NotificationActionButton1"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="@dimen/action_button_height"
+            android:minWidth="@dimen/action_button_min_width"
+            android:paddingBottom="@dimen/action_button_padding_bottom"
+            android:paddingTop="@dimen/action_button_padding_top"
+            android:visibility="gone"/>
+
+        <com.android.car.notification.template.CarNotificationActionButton
+            android:id="@+id/action_2"
+            style="@style/NotificationActionButton2"
+            android:layout_weight="1"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/action_button_height"
+            android:layout_marginStart="@dimen/action_button_spacing_start"
+            android:minWidth="@dimen/action_button_min_width"
+            android:paddingBottom="@dimen/action_button_padding_bottom"
+            android:paddingTop="@dimen/action_button_padding_top"
+            android:visibility="gone"/>
+
+        <com.android.car.notification.template.CarNotificationActionButton
+            android:id="@+id/action_3"
+            style="@style/NotificationActionButton3"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="@dimen/action_button_height"
+            android:layout_marginStart="@dimen/action_button_spacing_start"
+            android:minWidth="@dimen/action_button_min_width"
+            android:paddingBottom="@dimen/action_button_padding_bottom"
+            android:paddingTop="@dimen/action_button_padding_top"
+            android:visibility="gone"/>
+
+    </LinearLayout>
+</merge>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/headsup_container_bottom.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/headsup_container_bottom.xml
new file mode 100644
index 0000000..3cff34a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/headsup_container_bottom.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/notification_headsup"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.android.car.notification.headsup.HeadsUpContainerView
+        android:id="@+id/headsup_content"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_marginBottom="@dimen/headsup_notification_bottom_margin"/>
+
+</RelativeLayout>
\ No newline at end of file
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
new file mode 100644
index 0000000..4fd0492
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/message_headsup_notification_template.xml
@@ -0,0 +1,74 @@
+<?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.
+  -->
+
+<com.android.car.ui.FocusArea
+    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="wrap_content">
+
+    <androidx.cardview.widget.CardView
+        android:id="@+id/card_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        app:cardCornerRadius="@dimen/notification_card_radius">
+
+        <RelativeLayout
+            android:id="@+id/inner_template_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/car_notification_card_inner_top_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_alignParentStart="true"
+                app:isHeadsUp="true"/>
+
+            <com.android.car.notification.template.CarNotificationBodyView
+                android:id="@+id/notification_body"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="@dimen/notification_touch_target_size"
+                android:gravity="center_vertical"
+                android:layout_alignParentTop="true"
+                android:layout_alignParentStart="true"
+                android:layout_alignParentEnd="true"
+                android:layout_marginStart="@dimen/card_body_margin_start"
+                app:isHeadsUp="true"
+                app:maxLines="@integer/config_headsUpNotificationMaxBodyLines"
+                app:showBigIcon="true"/>
+
+            <FrameLayout
+                android:id="@+id/notification_actions_wrapper"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_below="@id/notification_body">
+
+                <com.android.car.notification.template.CarNotificationActionsView
+                    android:id="@+id/notification_actions"
+                    style="@style/NotificationActionViewLayout"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"/>
+
+            </FrameLayout>
+        </RelativeLayout>
+    </androidx.cardview.widget.CardView>
+</com.android.car.ui.FocusArea>
\ No newline at end of file
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-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..0cc16f9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values-night/colors.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.
+  -->
+
+<resources>
+    <color name="action_button_background_color">#2e3134</color>
+    <color name="primary_text_color">@android:color/system_neutral1_50</color>
+    <color name="secondary_text_color">@android:color/system_neutral2_400</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/colors.xml
new file mode 100644
index 0000000..6cf3e5a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/colors.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.
+  -->
+
+<resources>
+    <color name="action_button_background_color">#e8eaed</color>
+    <color name="clear_all_button_background_color">@color/action_button_background_color</color>
+    <color name="primary_text_color">@android:color/system_neutral1_900</color>
+    <color name="secondary_text_color">@android:color/system_neutral2_500</color>
+    <color name="icon_tint">@color/primary_text_color</color>
+
+    <color name="call_accept_button">#29cb86</color>
+    <color name="call_decline_button">#e46962</color>
+</resources>
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
new file mode 100644
index 0000000..9e25a62
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/config.xml
@@ -0,0 +1,37 @@
+<?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="config_showHeadsUpNotificationOnBottom">true</bool>
+
+    <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/dimens.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/dimens.xml
new file mode 100644
index 0000000..32659ce
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/dimens.xml
@@ -0,0 +1,45 @@
+<?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>
+    <!-- Horizontal margin for HUNs -->
+    <dimen name="notification_headsup_card_margin_horizontal">0dp</dimen>
+    <dimen name="car_notification_card_inner_top_margin">8dp</dimen>
+
+    <!-- Card View -->
+    <dimen name="card_start_margin">16dp</dimen>
+    <dimen name="card_end_margin">8dp</dimen>
+    <dimen name="card_body_margin_bottom">8dp</dimen>
+    <dimen name="card_body_margin_start">16dp</dimen>
+    <dimen name="card_header_margin_bottom">8dp</dimen>
+    <dimen name="notification_card_radius">24dp</dimen>
+    <dimen name="headsup_notification_bottom_margin">160dp</dimen>
+
+    <!-- Icons -->
+    <dimen name="notification_touch_target_size">120dp</dimen>
+    <dimen name="body_big_icon_margin">8dp</dimen>
+
+    <!-- Action View -->
+    <dimen name="action_button_height">88dp</dimen>
+    <dimen name="action_button_radius">24dp</dimen>
+    <dimen name="action_view_left_margin">8dp</dimen>
+    <dimen name="action_view_right_margin">8dp</dimen>
+    <dimen name="action_button_padding_bottom">8dp</dimen>
+
+    <dimen name="card_min_bottom_padding">8dp</dimen>
+    <dimen name="card_min_top_padding">8dp</dimen>
+</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
new file mode 100644
index 0000000..981ffdc
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?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>
+    <!-- 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/values/styles.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/styles.xml
new file mode 100644
index 0000000..2727581
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/styles.xml
@@ -0,0 +1,66 @@
+<?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>
+    <style name="NotificationBodyTitleText" parent="@android:TextAppearance.DeviceDefault.Large">
+        <item name="android:maxLines">1</item>
+        <item name="android:ellipsize">end</item>
+        <item name="android:textAlignment">viewStart</item>
+    </style>
+
+    <style name="NotificationBodyContentText" parent="@android:TextAppearance.DeviceDefault.Small">
+        <item name="android:maxLines">1</item>
+        <item name="android:ellipsize">end</item>
+        <item name="android:textAlignment">viewStart</item>
+    </style>
+
+    <style name="NotificationActionViewLayout">
+        <item name="android:layout_gravity">center</item>
+        <item name="android:layout_marginLeft">8dp</item>
+        <item name="android:layout_marginRight">8dp</item>
+        <item name="android:layout_marginTop">8dp</item>
+        <item name="android:layout_marginBottom">8dp</item>
+    </style>
+
+    <style name="NotificationActionButtonBase" parent="@android:Widget.DeviceDefault.Button.Borderless.Colored">
+        <item name="android:minWidth">@dimen/action_button_min_width</item>
+        <item name="android:gravity">center</item>
+        <item name="android:textSize">32sp</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:textColor">@color/notification_accent_color</item>
+        <item name="android:maxLines">1</item>
+        <item name="android:ellipsize">end</item>
+        <item name="android:background">@drawable/action_button_background</item>
+    </style>
+
+    <style name="NotificationActionButtonText" parent="@android:TextAppearance.DeviceDefault.Large">
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:maxLines">1</item>
+        <item name="android:ellipsize">end</item>
+    </style>
+
+    <style name="ClearAllButton" parent="@android:Widget.DeviceDefault.Button.Borderless.Colored">
+        <item name="android:minWidth">@dimen/clear_all_button_min_width</item>
+        <item name="android:paddingStart">@dimen/clear_all_button_padding</item>
+        <item name="android:paddingEnd">@dimen/clear_all_button_padding</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:gravity">center</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:background">@drawable/clear_all_button_background</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/themes.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/themes.xml
new file mode 100644
index 0000000..36f372b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/themes.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.
+  -->
+
+<resources>
+    <style name="Theme.DeviceDefault.NoActionBar.Notification" parent="@android:Theme.DeviceDefault.NoActionBar">
+    </style>
+</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
new file mode 100644
index 0000000..18124ea
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/xml/overlays.xml
@@ -0,0 +1,102 @@
+<?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="attr/categoryCall" value="@attr/categoryCall"/>
+    <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="color/icon_tint" value="@color/icon_tint"/>
+    <item target="color/call_accept_button" value="@color/call_accept_button"/>
+    <item target="color/call_decline_button" value="@color/call_decline_button"/>
+
+    <item target="color/action_button_background_color" value="@color/action_button_background_color"/>
+    <item target="color/clear_all_button_background_color" value="@color/clear_all_button_background_color"/>
+    <item target="color/primary_text_color" value="@color/primary_text_color"/>
+    <item target="color/secondary_text_color" value="@color/secondary_text_color"/>
+
+    <item target="dimen/action_button_height" value="@dimen/action_button_height" />
+    <item target="dimen/action_button_radius" value="@dimen/action_button_radius" />
+    <item target="dimen/action_button_padding_bottom" value="@dimen/action_button_padding_bottom"/>
+    <item target="dimen/action_view_left_margin" value="@dimen/action_view_left_margin" />
+    <item target="dimen/action_view_right_margin" value="@dimen/action_view_right_margin" />
+    <item target="dimen/body_big_icon_margin" value="@dimen/body_big_icon_margin"/>
+    <item target="dimen/car_notification_card_inner_top_margin" value="@dimen/car_notification_card_inner_top_margin"/>
+    <item target="dimen/card_start_margin" value="@dimen/card_start_margin" />
+    <item target="dimen/card_body_margin_bottom" value="@dimen/card_body_margin_bottom" />
+    <item target="dimen/card_end_margin" value="@dimen/card_start_margin" />
+    <item target="dimen/card_header_margin_bottom" value="@dimen/card_header_margin_bottom" />
+    <item target="dimen/notification_card_radius" value="@dimen/notification_card_radius"/>
+    <item target="dimen/notification_headsup_card_margin_horizontal" value="@dimen/notification_headsup_card_margin_horizontal" />
+    <item target="dimen/notification_touch_target_size" value="@dimen/notification_touch_target_size"/>
+
+    <item target="id/action_1" value="@id/action_1" />
+    <item target="id/action_2" value="@id/action_2" />
+    <item target="id/action_3" value="@id/action_3" />
+    <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/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_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/ClearAllButton" value="@style/ClearAllButton"/>
+    <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="style/NotificationActionButtonText" value="@style/NotificationActionButtonText"/>
+    <item target="style/Theme.DeviceDefault.NoActionBar.Notification" value="@style/Theme.DeviceDefault.NoActionBar.Notification"/>
+
+    <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"/>
+
+    <item target="layout/car_headsup_notification_header_view" value="@layout/car_headsup_notification_header_view"/>
+    <item target="id/app_icon" value="@id/app_icon"/>
+    <item target="id/header_text" value="@id/header_text"/>
+
+    <item target="layout/car_headsup_notification_body_view" value="@layout/car_headsup_notification_body_view"/>
+    <item target="id/notification_body_icon" value="@id/notification_body_icon"/>
+    <item target="id/notification_body_title" value="@id/notification_body_title"/>
+    <item target="id/notification_body_content" value="@id/notification_body_content"/>
+
+    <item target="layout/call_headsup_notification_template" value="@layout/call_headsup_notification_template"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/Android.bp
new file mode 100644
index 0000000..b63e2fe
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/Android.bp
@@ -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.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+    name: "CarUiPortraitSettingsProviderRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/AndroidManifest.xml
new file mode 100644
index 0000000..807a91a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/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.android.providers.settings.caruiportrait.rro">
+    <application android:hasCode="false" />
+    <overlay android:priority="20"
+             android:targetName="SettingsProvider"
+             android:targetPackage="com.android.providers.settings"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/values/defaults.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/values/defaults.xml
new file mode 100644
index 0000000..39181e4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/values/defaults.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>
+    <!-- Set default screen orientation. -->
+    <integer name="def_user_rotation">0</integer>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..23a2903
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/xml/overlays.xml
@@ -0,0 +1,18 @@
+<!--
+  ~ 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="integer/def_user_rotation" value="@integer/def_user_rotation"/>
+</overlay>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/Android.bp
new file mode 100644
index 0000000..5b9f206
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/Android.bp
@@ -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.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+    name: "CarUiPortraitSettingsRRO",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/AndroidManifest.xml
new file mode 100644
index 0000000..8d15347
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/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.android.car.settings.caruiportrait.rro">
+    <application android:hasCode="false"/>
+    <overlay android:priority="20"
+             android:targetName="CarSettings"
+             android:targetPackage="com.android.car.settings"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_background.xml
new file mode 100644
index 0000000..59b968b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_background.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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_focused="true" android:state_pressed="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/top_level_preference_background_color"/>
+        </shape>
+    </item>
+    <item android:state_focused="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/top_level_preference_background_color"/>
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/top_level_preference_background_color"/>
+        </shape>
+    </item>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_highlight.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_highlight.xml
new file mode 100644
index 0000000..d2b21a3
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_highlight.xml
@@ -0,0 +1,31 @@
+<?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:right="100dip">
+        <shape
+            android:shape="rectangle" >
+            <solid android:color="#91AFC6" />
+        </shape>
+    </item>
+    <item android:left="8dip">
+        <shape
+            android:shape="rectangle" >
+            <solid android:color="@color/top_level_preference_background_color" />
+        </shape>
+    </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/layout/top_level_preference.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/layout/top_level_preference.xml
new file mode 100644
index 0000000..2466c5f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/layout/top_level_preference.xml
@@ -0,0 +1,67 @@
+<?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.
+  -->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:clipToPadding="false"
+    android:minHeight="96dp"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:tag="carUiPreference"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+    <com.android.car.ui.uxr.DrawableStateImageView
+        android:id="@android:id/icon"
+        android:layout_width="44dp"
+        android:layout_height="44dp"
+        android:layout_alignParentStart="true"
+        android:layout_centerVertical="true"
+        android:layout_marginStart="23dp"
+        android:scaleType="fitCenter"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginStart="33dp"
+        android:layout_toEndOf="@android:id/icon"
+        android:layout_toStartOf="@android:id/widget_frame"
+        android:orientation="vertical">
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
+    </LinearLayout>
+
+    <!-- Preference should place its actual preference widget here. -->
+    <FrameLayout
+        android:id="@android:id/widget_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..957a4b4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values-night/colors.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 xmlns:android="http://schemas.android.com/apk/res/android">
+    <color name="top_level_preference_background_color">#282A2D</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values/colors.xml
new file mode 100644
index 0000000..59a99d0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values/colors.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 xmlns:android="http://schemas.android.com/apk/res/android">
+    <color name="top_level_preference_background_color">#E8EAED</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..c37acee
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/xml/overlays.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.
+  -->
+<overlay>
+    <item target="drawable/top_level_preference_background" value="@drawable/top_level_preference_background"/>
+    <item target="drawable/top_level_preference_highlight" value="@drawable/top_level_preference_highlight"/>
+
+    <item target="layout/top_level_preference" value="@layout/top_level_preference"/>
+
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/android/Android.bp b/car_product/car_ui_portrait/rro/android/Android.bp
new file mode 100644
index 0000000..73878ea
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/Android.bp
@@ -0,0 +1,36 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+runtime_resource_overlay {
+    name: "CarUiPortraitFrameworkResRRO",
+    resource_dirs: ["res"],
+    certificate: "platform",
+    manifest: "AndroidManifest.xml",
+    system_ext_specific: true,
+}
+
+android_app {
+    name: "CarUiPortraitFrameworkResRROTest",
+    resource_dirs: ["res"],
+    platform_apis: true,
+    manifest: "AndroidManifest-test.xml",
+    aaptflags: [
+        "--no-resource-deduping",
+        "--no-resource-removal"
+    ],
+}
diff --git a/car_product/car_ui_portrait/rro/android/AndroidManifest-test.xml b/car_product/car_ui_portrait/rro/android/AndroidManifest-test.xml
new file mode 100644
index 0000000..0895ea8
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/AndroidManifest-test.xml
@@ -0,0 +1,24 @@
+<?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="android.caruiportrait.rro.test">
+    <application android:hasCode="false" />
+    <overlay
+        android:targetPackage="android"
+        android:priority="21"
+        android:category="caruiportrait.rro"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/android/AndroidManifest.xml b/car_product/car_ui_portrait/rro/android/AndroidManifest.xml
new file mode 100644
index 0000000..41a3491
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/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="android.caruiportrait.rro">
+    <application android:hasCode="false" />
+    <overlay
+        android:targetPackage="android"
+        android:isStatic="true"
+        android:priority="20"
+        android:category="caruiportrait.rro"/>
+</manifest>
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-night/text_color_on_accent_device_default.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_on_accent_device_default.xml
new file mode 100644
index 0000000..24f27f2
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_on_accent_device_default.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.
+  -->
+<!-- Please see primary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+          android:color="@*android:color/system_neutral1_900"/>
+    <item android:color="@*android:color/system_neutral1_400"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_dark.xml
new file mode 100644
index 0000000..c82f99c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_dark.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.
+  -->
+<!-- Please see primary_text_material_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+          android:color="@*android:color/system_neutral1_500"/>
+    <item android:color="@*android:color/system_neutral1_50"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_light.xml
new file mode 100644
index 0000000..c9b5a6b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_light.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.
+  -->
+<!-- Please see primary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+          android:color="@*android:color/system_neutral1_400"/>
+    <item android:color="@*android:color/system_neutral1_900"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_dark.xml
new file mode 100644
index 0000000..470ed75
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_dark.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.
+  -->
+<!-- Please see secondary_text_material_dark.xml -->
+<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_neutral2_200"/>
+    <item android:color="@*android:color/system_neutral2_200"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_light.xml
new file mode 100644
index 0000000..30759cc
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_light.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.
+  -->
+<!-- Please see secondary_text_material_light.xml -->
+<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_neutral2_700"/>
+    <item android:color="@*android:color/system_neutral2_700"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_dark.xml
new file mode 100644
index 0000000..f3d50db
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_dark.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.
+  -->
+<!-- Please see tertiary_text_material_dark.xml -->
+<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_neutral2_400"/>
+    <item android:color="@*android:color/system_neutral2_400"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_light.xml
new file mode 100644
index 0000000..638084e
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_light.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.
+  -->
+<!-- Please see tertiary_text_material_light.xml -->
+<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_neutral2_500"/>
+    <item android:color="@*android:color/system_neutral2_500"/>
+</selector>
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
new file mode 100644
index 0000000..037fe3e
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/btn_device_default_dark.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.
+  -->
+
+<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_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/color/text_color_on_accent_device_default.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_on_accent_device_default.xml
new file mode 100644
index 0000000..c9b5a6b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_on_accent_device_default.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.
+  -->
+<!-- Please see primary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+          android:color="@*android:color/system_neutral1_400"/>
+    <item android:color="@*android:color/system_neutral1_900"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_dark.xml
new file mode 100644
index 0000000..0bb281f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_dark.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.
+  -->
+<!-- Please see primary_text_material_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+          android:color="@*android:color/system_neutral1_400"/>
+    <item android:color="@*android:color/system_neutral1_900"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_light.xml
new file mode 100644
index 0000000..9729379
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_light.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.
+  -->
+<!-- Please see primary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false"
+          android:color="@*android:color/system_neutral1_500"/>
+    <item android:color="@*android:color/system_neutral1_50"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_dark.xml
new file mode 100644
index 0000000..4b6483f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_dark.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.
+  -->
+<!-- Please see secondary_text_material_dark.xml -->
+<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_neutral2_700"/>
+    <item android:color="@*android:color/system_neutral2_700"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_light.xml
new file mode 100644
index 0000000..0d9f871
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_light.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.
+  -->
+<!-- Please see secondary_text_material_light.xml -->
+<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_neutral2_200"/>
+    <item android:color="@*android:color/system_neutral2_200"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_dark.xml
new file mode 100644
index 0000000..1cf8447
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_dark.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.
+  -->
+<!-- Please see tertiary_text_material_dark.xml -->
+<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_neutral2_500"/>
+    <item android:color="@*android:color/system_neutral2_500"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_light.xml
new file mode 100644
index 0000000..45160e3
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_light.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.
+  -->
+<!-- Please see tertiary_text_material_light.xml -->
+<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_neutral2_400"/>
+    <item android:color="@*android:color/system_neutral2_400"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/drawable/alert_dialog_bg.xml b/car_product/car_ui_portrait/rro/android/res/drawable/alert_dialog_bg.xml
new file mode 100644
index 0000000..b01931d
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/alert_dialog_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">
+    <corners android:radius="@dimen/alert_dialog_corner_radius"/>
+</shape>
\ 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
new file mode 100644
index 0000000..0693426
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/btn_borderless_car.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_focused="true">
+        <inset android:insetLeft="@*android:dimen/button_inset_horizontal_material"
+               android:insetTop="@*android:dimen/button_inset_vertical_material"
+               android:insetRight="@*android:dimen/button_inset_horizontal_material"
+               android:insetBottom="@*android:dimen/button_inset_vertical_material">
+            <selector>
+                <item android:state_focused="true" android:state_pressed="true">
+                    <shape android:shape="rectangle">
+                        <corners android:radius="?android:attr/buttonCornerRadius" />
+                        <solid android:color="#8A94CBFF"/>
+                        <stroke android:width="4dp"
+                                android:color="#94CBFF"/>
+                        <padding android:left="@*android:dimen/button_padding_horizontal_material"
+                                 android:top="@*android:dimen/button_padding_vertical_material"
+                                 android:right="@*android:dimen/button_padding_horizontal_material"
+                                 android:bottom="@*android:dimen/button_padding_vertical_material" />
+                    </shape>
+                </item>
+                <item android:state_focused="true">
+                    <shape android:shape="rectangle">
+                        <corners android:radius="?android:attr/buttonCornerRadius" />
+                        <solid android:color="#3D94CBFF"/>
+                        <stroke android:width="8dp" android:color="#94CBFF"/>
+                        <padding android:left="@*android:dimen/button_padding_horizontal_material"
+                                 android:top="@*android:dimen/button_padding_vertical_material"
+                                 android:right="@*android:dimen/button_padding_horizontal_material"
+                                 android:bottom="@*android:dimen/button_padding_vertical_material" />
+                    </shape>
+                </item>
+            </selector>
+        </inset>
+    </item>
+    <item>
+        <ripple xmlns:android="http://schemas.android.com/apk/res/android"
+        android:color="?android:attr/colorControlHighlight">
+            <item android:id="@android:id/mask"
+                  android:drawable="@*android:drawable/btn_default_mtrl_shape" />
+        </ripple>
+    </item>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/drawable/car_dialog_button_background.xml b/car_product/car_ui_portrait/rro/android/res/drawable/car_dialog_button_background.xml
new file mode 100644
index 0000000..a551f99
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/car_dialog_button_background.xml
@@ -0,0 +1,32 @@
+<?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>
+        <shape>
+            <solid android:color="@color/car_alert_dialog_action_button_color"/>
+            <corners android:radius="@dimen/alert_dialog_button_corner_radius"/>
+        </shape>
+    </item>
+    <item>
+        <ripple android:color="?android:attr/colorControlHighlight">
+            <item android:id="@android:id/mask">
+                <color android:color="@*android:color/car_white_1000"/>
+            </item>
+        </ripple>
+    </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/rro/android/res/drawable/toast_frame.xml b/car_product/car_ui_portrait/rro/android/res/drawable/toast_frame.xml
new file mode 100644
index 0000000..8b829db
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/toast_frame.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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="#2E3134" />
+    <corners android:radius="@dimen/toast_corner_radius" />
+</shape>
diff --git a/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog.xml b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog.xml
new file mode 100644
index 0000000..272e274
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog.xml
@@ -0,0 +1,77 @@
+<?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.
+  -->
+<com.android.internal.widget.AlertDialogLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@*android:id/parentPanel"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="400dp"
+    android:maxHeight="916dp"
+    android:gravity="start|top"
+    android:background="@color/alert_dialog_background_color"
+    android:orientation="vertical">
+    <include layout="@layout/car_alert_dialog_title" />
+    <FrameLayout
+        android:id="@*android:id/contentPanel"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/car_alert_dialog_margin"
+        android:layout_marginStart="@dimen/car_alert_dialog_margin"
+        android:layout_marginEnd="@dimen/car_alert_dialog_margin">
+        <ScrollView
+            android:id="@*android:id/scrollView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clipToPadding="false">
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+                <Space
+                    android:id="@*android:id/textSpacerNoTitle"
+                    android:visibility="gone"
+                    android:layout_width="match_parent"
+                    android:layout_height="36dp"/>
+                <TextView
+                    android:id="@android:id/message"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:textColor="@color/alert_dialog_message_text_color"/>
+                <!-- we don't need this spacer, but the id needs to be here for compatibility -->
+                <Space
+                    android:id="@*android:id/textSpacerNoButtons"
+                    android:visibility="gone"
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp" />
+            </LinearLayout>
+        </ScrollView>
+    </FrameLayout>
+    <FrameLayout
+        android:id="@*android:id/customPanel"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="48dp">
+        <FrameLayout
+            android:id="@android:id/custom"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+    </FrameLayout>
+    <include
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        layout="@layout/car_alert_dialog_button_bar" />
+</com.android.internal.widget.AlertDialogLayout>
diff --git a/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_button_bar.xml b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_button_bar.xml
new file mode 100644
index 0000000..72294cc
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_button_bar.xml
@@ -0,0 +1,63 @@
+<?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.
+  -->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            android:id="@*android:id/buttonPanel"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:scrollbarAlwaysDrawVerticalTrack="true"
+            android:scrollIndicators="top|bottom"
+            android:fillViewport="true"
+            style="?android:attr/buttonBarStyle">
+    <com.android.internal.widget.ButtonBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layoutDirection="locale"
+        android:layout_marginTop="@dimen/car_alert_dialog_margin"
+        android:layout_marginLeft="@dimen/car_alert_dialog_margin"
+        android:layout_marginRight="@dimen/car_alert_dialog_margin"
+        android:orientation="horizontal"
+        android:gravity="center">
+        <Button
+            android:id="@android:id/button3"
+            android:background="@drawable/car_dialog_button_background"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/button_layout_height"
+            android:layout_marginRight="@dimen/car_alert_dialog_button_margin"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="@color/alert_dialog_message_text_color"/>
+        <Button
+            android:id="@android:id/button2"
+            android:background="@drawable/car_dialog_button_background"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/button_layout_height"
+            android:layout_marginRight="@dimen/car_alert_dialog_button_margin"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="@color/alert_dialog_message_text_color"/>
+        <Button
+            android:id="@android:id/button1"
+            android:background="@drawable/car_dialog_button_background"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/button_layout_height"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="@color/alert_dialog_message_text_color"/>
+        <Space
+            android:id="@*android:id/spacer"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:visibility="gone" />
+    </com.android.internal.widget.ButtonBarLayout>
+</ScrollView>
diff --git a/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_title.xml b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_title.xml
new file mode 100644
index 0000000..3e08602
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_title.xml
@@ -0,0 +1,53 @@
+<?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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:id="@*android:id/topPanel"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:gravity="center_vertical"
+              android:orientation="vertical">
+    <!-- If the client uses a customTitle, it will be added here. -->
+    <RelativeLayout
+        android:id="@*android:id/title_template"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_card_header_height"
+        android:orientation="horizontal">
+        <ImageView
+            android:id="@android:id/icon"
+            android:layout_width="44dp"
+            android:layout_height="44dp"
+            android:layout_alignParentStart="true"
+            android:layout_centerVertical="true"
+            android:scaleType="fitCenter"
+            android:src="@null" />
+        <com.android.internal.widget.DialogTitle
+            android:id="@*android:id/alertTitle"
+            android:maxLines="1"
+            android:ellipsize="none"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_toEndOf="@+id/icon"
+            android:textAlignment="viewStart"
+            android:textColor="@color/alert_dialog_message_text_color"
+            android:layout_centerVertical="true"/>
+    </RelativeLayout>
+    <Space
+        android:id="@*android:id/titleDividerNoCustom"
+        android:visibility="gone"
+        android:layout_width="match_parent"
+        android:layout_height="0dp" />
+</LinearLayout>
diff --git a/car_product/car_ui_portrait/rro/android/res/layout/transient_notification.xml b/car_product/car_ui_portrait/rro/android/res/layout/transient_notification.xml
new file mode 100644
index 0000000..2eeaa7a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/layout/transient_notification.xml
@@ -0,0 +1,40 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:maxWidth="@dimen/toast_width"
+    android:background="@drawable/toast_frame"
+    android:elevation="@dimen/toast_elevation"
+    android:paddingEnd="@dimen/toast_margin"
+    android:paddingTop="@dimen/toast_margin"
+    android:paddingBottom="@dimen/toast_margin"
+    android:paddingStart="@dimen/toast_margin"
+    android:layout_marginBottom="@dimen/toast_bottom_margin">
+
+    <TextView
+        android:id="@android:id/message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:maxLines="2"
+        android:textAppearance="@style/TextAppearance_Toast"/>
+</LinearLayout>
diff --git a/car_product/car_ui_portrait/rro/android/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/android/res/values-night/colors.xml
new file mode 100644
index 0000000..2a70fa4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values-night/colors.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.
+  -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <color name="alert_dialog_background_color">#202124</color>
+    <color name="alert_dialog_message_text_color">#fff</color>
+    <color name="car_alert_dialog_action_button_color">#2E3134</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values-night/colors_device_default.xml b/car_product/car_ui_portrait/rro/android/res/values-night/colors_device_default.xml
new file mode 100644
index 0000000..2328a0b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values-night/colors_device_default.xml
@@ -0,0 +1,85 @@
+<?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">
+
+    <color name="primary_device_default_dark">@*android:color/system_neutral1_900</color>
+    <color name="primary_device_default_light">@*android:color/system_neutral1_50</color>
+    <color name="primary_device_default_settings">@*android:color/system_neutral1_900</color>
+    <color name="primary_device_default_settings_light">@*android:color/primary_device_default_light</color>
+    <color name="primary_dark_device_default_dark">@*android:color/primary_device_default_dark</color>
+    <color name="primary_dark_device_default_light">@*android:color/primary_device_default_light</color>
+    <color name="primary_dark_device_default_settings">@*android:color/primary_device_default_dark</color>
+    <color name="primary_dark_device_default_settings_light">@*android:color/primary_device_default_light</color>
+    <color name="secondary_device_default_settings">@*android:color/secondary_material_settings</color>
+    <color name="secondary_device_default_settings_light">@*android:color/secondary_material_settings_light</color>
+    <color name="tertiary_device_default_settings">@*android:color/tertiary_material_settings</color>
+    <color name="quaternary_device_default_settings">@*android:color/quaternary_material_settings</color>
+    <color name="navigation_bar_divider_device_default_settings">#1f000000</color>
+
+    <!--  Accent colors  -->
+    <color name="accent_device_default_light">@*android:color/system_accent1_600</color>
+    <color name="accent_device_default_dark">@*android:color/system_accent1_100</color>
+    <color name="accent_device_default">@*android:color/accent_device_default_light</color>
+    <color name="accent_primary_device_default">@*android:color/system_accent1_100</color>
+    <color name="accent_secondary_device_default">@*android:color/system_accent2_100</color>
+    <color name="accent_tertiary_device_default">@*android:color/system_accent3_100</color>
+
+    <!-- Accent variants -->
+    <color name="accent_primary_variant_light_device_default">@*android:color/system_accent1_600</color>
+    <color name="accent_secondary_variant_light_device_default">@*android:color/system_accent2_600</color>
+    <color name="accent_tertiary_variant_light_device_default">@*android:color/system_accent3_600</color>
+    <color name="accent_primary_variant_dark_device_default">@*android:color/system_accent1_300</color>
+    <color name="accent_secondary_variant_dark_device_default">@*android:color/system_accent2_300</color>
+    <color name="accent_tertiary_variant_dark_device_default">@*android:color/system_accent3_300</color>
+
+    <!-- Background colors -->
+    <color name="background_device_default_dark">@*android:color/system_neutral1_1000</color>
+    <color name="background_device_default_light">@*android:color/system_neutral1_50</color>
+    <color name="background_floating_device_default_dark">@*android:color/car_grey_900</color>
+    <color name="background_floating_device_default_light">@*android:color/background_device_default_light</color>
+
+    <!-- Surface colors -->
+    <color name="surface_header_dark">@*android:color/system_neutral1_700</color>
+    <color name="surface_header_light">@*android:color/system_neutral1_100</color>
+    <color name="surface_variant_dark">@*android:color/system_neutral1_700</color>
+    <color name="surface_variant_light">@*android:color/system_neutral2_100</color>
+    <color name="surface_dark">@*android:color/system_neutral1_800</color>
+    <color name="surface_highlight_light">@*android:color/system_neutral1_0</color>
+
+    <!-- Please refer to text_color_[primary]_device_default_[light].xml for text colors-->
+    <color name="foreground_device_default_light">@*android:color/text_color_primary_device_default_light</color>
+    <color name="foreground_device_default_dark">@*android:color/text_color_primary_device_default_dark</color>
+
+    <!-- Error color -->
+    <color name="error_color_device_default_dark">@*android:color/error_color_material_dark</color>
+    <color name="error_color_device_default_light">@*android:color/error_color_material_light</color>
+
+    <color name="list_divider_color_light">@*android:color/system_neutral1_200</color>
+    <color name="list_divider_color_dark">@*android:color/system_neutral1_700</color>
+    <color name="list_divider_opacity_device_default_light">@android:color/white</color>
+    <color name="list_divider_opacity_device_default_dark">@android:color/white</color>
+
+    <color name="loading_gradient_background_color_dark">#44484C</color>
+    <color name="loading_gradient_background_color_light">#F8F9FA</color>
+    <color name="loading_gradient_highlight_color_dark">#4D5155</color>
+    <color name="loading_gradient_highlight_color_light">#F1F3F4</color>
+
+    <color name="edge_effect_device_default_light">@android:color/black</color>
+    <color name="edge_effect_device_default_dark">@android:color/white</color>
+
+    <color name="floating_background_color">@*android:color/car_grey_900</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values-sw900dp/dimens.xml b/car_product/car_ui_portrait/rro/android/res/values-sw900dp/dimens.xml
new file mode 100644
index 0000000..177ff6d
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values-sw900dp/dimens.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.
+  -->
+<resources>
+    <!-- Height of the bottom navigation / climate bar. -->
+    <!--    TODO: remove-->
+    <dimen name="navigation_bar_height">160dp</dimen>
+    <dimen name="navigation_bar_height_landscape">160dp</dimen>
+</resources>
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
new file mode 100644
index 0000000..22c0af0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/colors.xml
@@ -0,0 +1,71 @@
+<?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">
+  <color name="car_alert_dialog_action_button_color">#dadce0</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-->
+  <color name="system_accent1_10">#defbff</color>
+  <color name="system_accent1_50">#defbff</color>
+  <color name="system_accent1_100">#acf6fe</color>
+  <color name="system_accent1_200">#6bf0ff</color>
+  <color name="system_accent1_300">#00e8fe</color>
+  <color name="system_accent1_400">#00e1fa</color>
+  <color name="system_accent1_500">#00daf8</color>
+  <color name="system_accent1_600">#00c9e3</color>
+  <color name="system_accent1_700">#00b2c7</color>
+  <color name="system_accent1_800">#009eae</color>
+  <color name="system_accent1_900">#00797f</color>
+  <color name="system_accent1_1000">#000000</color>
+
+  <color name="system_accent2_0">#ffffff</color>
+  <color name="system_accent2_10">#e5f2ff</color>
+  <color name="system_accent2_50">#e5f2ff</color>
+  <color name="system_accent2_100">#c8deed</color>
+  <color name="system_accent2_200">#aec7da</color>
+  <color name="system_accent2_300">#91afc6</color>
+  <color name="system_accent2_400">#7b9cb5</color>
+  <color name="system_accent2_500">#648aa6</color>
+  <color name="system_accent2_600">#567b94</color>
+  <color name="system_accent2_700">#46667c</color>
+  <color name="system_accent2_800">#385366</color>
+  <color name="system_accent2_900">#263d4e</color>
+  <color name="system_accent2_1000">#000000</color>
+
+  <color name="system_accent3_0">#ffffff</color>
+  <color name="system_accent3_10">#e2f8ed</color>
+  <color name="system_accent3_50">#e2f8ed</color>
+  <color name="system_accent3_100">#b9eed2</color>
+  <color name="system_accent3_200">#8de2b7</color>
+  <color name="system_accent3_300">#5cd69b</color>
+  <color name="system_accent3_400">#29cb86</color>
+  <color name="system_accent3_500">#00c171</color>
+  <color name="system_accent3_600">#00b166</color>
+  <color name="system_accent3_700">#009e59</color>
+  <color name="system_accent3_800">#008c4d</color>
+  <color name="system_accent3_900">#006c37</color>
+  <color name="system_accent3_1000">#000000</color>
+
+  <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>
+  <color name="alert_dialog_background_color">#f1f3f4</color>
+  <color name="alert_dialog_message_text_color">#000</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/colors_device_default.xml b/car_product/car_ui_portrait/rro/android/res/values/colors_device_default.xml
new file mode 100644
index 0000000..c9e8d2d
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/colors_device_default.xml
@@ -0,0 +1,83 @@
+<?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">
+
+    <!--title bars-->
+    <color name="primary_device_default_dark">@*android:color/system_neutral1_200</color>
+    <color name="primary_device_default_light">@*android:color/system_neutral1_300</color>
+
+    <color name="primary_device_default_settings">@*android:color/system_neutral1_200</color>
+    <color name="primary_device_default_settings_light">@*android:color/primary_device_default_light</color>
+    <color name="primary_dark_device_default_dark">@*android:color/primary_device_default_dark</color>
+    <color name="primary_dark_device_default_light">@*android:color/primary_device_default_light</color>
+    <color name="primary_dark_device_default_settings">@*android:color/primary_device_default_dark</color>
+    <color name="primary_dark_device_default_settings_light">@*android:color/primary_device_default_light</color>
+    <color name="secondary_device_default_settings">@*android:color/secondary_material_settings</color>
+    <color name="secondary_device_default_settings_light">@*android:color/secondary_material_settings_light</color>
+    <color name="tertiary_device_default_settings">@*android:color/tertiary_material_settings</color>
+    <color name="quaternary_device_default_settings">@*android:color/quaternary_material_settings</color>
+    <color name="navigation_bar_divider_device_default_settings">#1f000000</color>
+
+    <!--  Accent colors edit  -->
+    <color name="accent_device_default_light">@*android:color/system_accent1_200</color>
+    <color name="accent_device_default_dark">@*android:color/system_accent1_200</color>
+    <color name="accent_device_default">@*android:color/accent_device_default_light</color>
+    <color name="accent_primary_device_default">@*android:color/system_accent1_200</color>
+    <color name="accent_secondary_device_default">@*android:color/system_accent2_300</color>
+    <color name="accent_tertiary_device_default">@*android:color/system_accent3_300</color>
+
+    <!-- Accent variants edit -->
+    <color name="accent_primary_variant_light_device_default">@*android:color/system_accent1_200</color>
+    <color name="accent_secondary_variant_light_device_default">@*android:color/system_accent2_300</color>
+    <color name="accent_tertiary_variant_light_device_default">@*android:color/system_accent3_300</color>
+    <color name="accent_primary_variant_dark_device_default">@*android:color/system_accent1_300</color>
+    <color name="accent_secondary_variant_dark_device_default">@*android:color/system_accent2_300</color>
+    <color name="accent_tertiary_variant_dark_device_default">@*android:color/system_accent3_300</color>
+
+    <!-- Background colors -->
+    <color name="background_device_default_dark">@*android:color/system_neutral1_0</color>
+    <color name="background_device_default_light">@*android:color/system_neutral1_900</color>
+    <color name="background_floating_device_default_dark">@*android:color/car_grey_300</color>
+    <color name="background_floating_device_default_light">@*android:color/background_device_default_light</color>
+
+    <!-- Surface colors -->
+    <color name="surface_header_dark">@*android:color/system_neutral1_100</color>
+    <color name="surface_header_light">@*android:color/system_neutral1_700</color>
+    <color name="surface_variant_dark">@*android:color/system_neutral1_100</color>
+    <color name="surface_variant_light">@*android:color/system_neutral2_700</color>
+    <color name="surface_dark">@*android:color/system_neutral1_200</color>
+    <color name="surface_highlight_light">@*android:color/system_neutral1_1000</color>
+
+    <!-- Please refer to text_color_[primary]_device_default_[light].xml for text colors-->
+    <color name="foreground_device_default_light">@*android:color/text_color_primary_device_default_light</color>
+    <color name="foreground_device_default_dark">@*android:color/text_color_primary_device_default_dark</color>
+
+    <color name="list_divider_color_light">@*android:color/system_neutral1_700</color>
+    <color name="list_divider_color_dark">@*android:color/system_neutral1_200</color>
+    <color name="list_divider_opacity_device_default_light">@android:color/black</color>
+    <color name="list_divider_opacity_device_default_dark">@android:color/black</color>
+
+    <color name="loading_gradient_background_color_dark">#F8F9FA</color>
+    <color name="loading_gradient_background_color_light">#44484C</color>
+    <color name="loading_gradient_highlight_color_dark">#F1F3F4</color>
+    <color name="loading_gradient_highlight_color_light">#4D5155</color>
+
+    <color name="edge_effect_device_default_light">@android:color/white</color>
+    <color name="edge_effect_device_default_dark">@android:color/black</color>
+
+    <color name="floating_background_color">@*android:color/car_grey_300</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/config.xml b/car_product/car_ui_portrait/rro/android/res/values/config.xml
new file mode 100644
index 0000000..8ab71a7
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/config.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.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- IME should not hide nav bar -->
+    <bool name="config_automotiveHideNavBarForKeyboard">false</bool>
+
+    <!-- Class name of the device specific implementation of DisplayAreaPolicy.Provider
+    or empty if the default should be used. -->
+    <string translatable="false" name="config_deviceSpecificDisplayAreaPolicyProvider">
+        com.android.server.wm.CarDisplayAreaPolicyProvider
+    </string>
+
+    <!-- Colon separated list of package names that should be granted Notification Listener access -->
+    <string name="config_defaultListenerAccessPackages" translatable="false">com.android.car.notification</string>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/dimens.xml b/car_product/car_ui_portrait/rro/android/res/values/dimens.xml
new file mode 100644
index 0000000..7a325ad
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/dimens.xml
@@ -0,0 +1,54 @@
+<?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>
+    <!-- Height of the status bar -->
+    <dimen name="status_bar_height">92dp</dimen>
+    <!-- Height of the bottom navigation / climate bar. -->
+    <dimen name="navigation_bar_height">160dp</dimen>
+    <dimen name="navigation_bar_height_landscape">160dp</dimen>
+
+    <!-- ****** Alert dialog dimens ***** -->
+
+    <!-- Dialog corner radius -->
+    <dimen name="alert_dialog_corner_radius">24dp</dimen>
+    <!-- Dialog button corner radius -->
+    <dimen name="alert_dialog_button_corner_radius">16dp</dimen>
+    <!-- Dialog header size -->
+    <dimen name="car_card_header_height">88dp</dimen>
+    <!-- Dialog image size -->
+    <dimen name="car_alert_dialog_title_image_size">@dimen/car_card_header_height</dimen>
+    <!-- Default dialog margin -->
+    <dimen name="car_alert_dialog_margin">36dp</dimen>
+    <!-- Dialog button layout height -->
+    <dimen name="button_layout_height">88dp</dimen>
+    <!-- Default dialog button margin -->
+    <dimen name="car_alert_dialog_button_margin">24dp</dimen>
+
+    <!-- ****** Toast dimens ***** -->
+
+    <!-- Toast corner radius -->
+    <dimen name="toast_corner_radius">24dp</dimen>
+    <!-- Toast margin -->
+    <dimen name="toast_margin">24dp</dimen>
+    <!-- Toast elevation -->
+    <dimen name="toast_elevation">2dp</dimen>
+    <!-- Toast max width -->
+    <dimen name="toast_width">760dp</dimen>
+    <!-- Toast y offset (should be the same as the height of the audio bar -->
+    <dimen name="toast_y_offset">136dp</dimen>
+    <dimen name="toast_bottom_margin">32dp</dimen>
+</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
new file mode 100644
index 0000000..c32411b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/styles.xml
@@ -0,0 +1,71 @@
+<?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>
+    <style name="DialogActionButton">
+        <item name="android:textSize">32sp</item>
+        <item name="android:textColor">@android:color/white</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:background">@drawable/car_dialog_button_background</item>
+    </style>
+
+    <style name="TextAppearance_Toast">
+        <item name="android:textColorHighlight">?android:textColorHighlight</item>
+        <item name="android:textColorHint">?android:textColorHint</item>
+        <item name="android:textColorLink">?android:textColorLink</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+        <item name="android:textSize">28sp</item>
+        <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
new file mode 100644
index 0000000..fadcfd5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/styles_device_default.xml
@@ -0,0 +1,164 @@
+<?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 is an override of frameworks/base/core/res/res/values/styles_device_default.xml
+    It is how the device default is changed to match the desired look for a car theme.
+-->
+<resources>
+
+    <style name="TextAppearance.DeviceDefault" parent="android:TextAppearance.Material.Large">
+        <item name="android:textSize">@*android:dimen/car_body3_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+    </style>
+    <style name="TextAppearance.DeviceDefault.Inverse" parent="android:TextAppearance.Material.Inverse">
+        <item name="android:textSize">@*android:dimen/car_body3_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.Large" parent="android:TextAppearance.Material.Large">
+        <item name="android:textSize">@*android:dimen/car_body1_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+    </style>
+    <style name="TextAppearance.DeviceDefault.Large.Inverse" parent="android:TextAppearance.Material.Large.Inverse">
+        <item name="android:textSize">@*android:dimen/car_body1_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.Medium" parent="android:TextAppearance.Material.Medium">
+        <item name="android:textSize">@*android:dimen/car_body2_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+    </style>
+    <style name="TextAppearance.DeviceDefault.Medium.Inverse" parent="android:TextAppearance.Material.Medium.Inverse">
+        <item name="android:textSize">@*android:dimen/car_body2_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.Small" parent="android:TextAppearance.Material.Small">
+        <item name="android:textSize">@*android:dimen/car_body4_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+    </style>
+    <style name="TextAppearance.DeviceDefault.Small.Inverse" parent="android:TextAppearance.Material.Small.Inverse">
+        <item name="android:textSize">@*android:dimen/car_body4_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.Subhead" parent="android:TextAppearance.Material.Subhead">
+        <item name="android:textSize">@*android:dimen/car_body1_size</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.Widget.Button.Borderless.Colored"
+           parent="android:TextAppearance.DeviceDefault.Widget.Button">
+        <item name="android:textColor">@*android:color/car_borderless_button_text_color</item>
+    </style>
+
+    <style name="DialogWindowTitle.DeviceDefault" parent="*android:DialogWindowTitle.Material">
+        <item name="android:textAppearance">@*android:style/TextAppearance.DeviceDefault.DialogWindowTitle</item>
+    </style>
+    <style name="TextAppearance.DeviceDefault.DialogWindowTitle" parent="android:TextAppearance.Material.DialogWindowTitle">
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@*android:dimen/car_body2_size</item>
+        <item name="android:textColor">@*android:color/car_body2</item>
+    </style>
+    <style name="TextAppearance.Material.DialogWindowTitle" parent="android:TextAppearance.Material.Title" />
+    <style name="TextAppearance.Material.Title" parent="android:TextAppearance.Material">
+        <item name="android:textSize">@*android:dimen/text_size_title_material</item>
+        <item name="android:fontFamily">@*android:string/font_family_title_material</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.Widget.Button" parent="android:TextAppearance.Material.Widget.Button">
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:textAllCaps">@*android:bool/config_buttonTextAllCaps</item>
+        <item name="android:textSize">@*android:dimen/car_action1_size</item>
+        <item name="android:textColor">@*android:color/car_button_text_color</item>
+    </style>
+
+    <style name="Widget.DeviceDefault.TextView" parent="android:Widget.Material.TextView">
+        <item name="android:ellipsize">none</item>
+        <item name="android:textSize">@*android:dimen/car_body1_size</item>
+        <item name="android:requiresFadingEdge">horizontal</item>
+        <item name="android:fadingEdgeLength">@*android:dimen/car_textview_fading_edge_length</item>
+    </style>
+
+    <style name="Widget.DeviceDefault.Button" parent="android:Widget.Material.Button">
+        <item name="android:singleLine">true</item>
+        <item name="android:ellipsize">none</item>
+        <item name="android:requiresFadingEdge">horizontal</item>
+        <item name="android:fadingEdgeLength">@*android:dimen/car_textview_fading_edge_length</item>
+        <item name="android:background">@*android:drawable/car_button_background</item>
+        <item name="android:layout_height">@*android:dimen/car_button_height</item>
+        <item name="android:minWidth">@*android:dimen/car_button_min_width</item>
+        <item name="android:paddingStart">@*android:dimen/car_button_horizontal_padding</item>
+        <item name="android:paddingEnd">@*android:dimen/car_button_horizontal_padding</item>
+    </style>
+
+    <style name="Widget.DeviceDefault.Button.Borderless" parent="android:Widget.Material.Button.Borderless">
+        <item name="android:background">@drawable/btn_borderless_car</item>
+    </style>
+
+    <style name="Widget.DeviceDefault.CompoundButton.CheckBox" parent="android:Widget.Material.CompoundButton.CheckBox">
+        <item name="android:button">@*android:drawable/car_checkbox</item>
+    </style>
+
+    <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">
+        <item name="android:minHeight">@*android:dimen/car_progress_bar_height</item>
+        <item name="android:maxHeight">@*android:dimen/car_progress_bar_height</item>
+    </style>
+
+    <style name="Widget.DeviceDefault.SeekBar" parent="android:Widget.Material.SeekBar">
+        <item name="android:progressDrawable">@*android:drawable/car_seekbar_track</item>
+        <item name="android:thumb">@*android:drawable/car_seekbar_thumb</item>
+    </style>
+
+    <style name="Widget.DeviceDefault.ActionBar.Solid" parent="android:Widget.Material.ActionBar.Solid">
+        <item name="android:textSize">@*android:dimen/car_body3_size</item>
+    </style>
+
+    <!-- Preference Styles -->
+    <style name="Preference.DeviceDefault" parent="*android:Preference.Material">
+        <item name="android:layout">@*android:layout/car_preference</item>
+    </style>
+    <style name="Preference.DeviceDefault.Category" parent="*android:Preference.Material.Category">
+        <item name="android:layout">@*android:layout/car_preference_category</item>
+    </style>
+    <style name="Preference.DeviceDefault.CheckBoxPreference" parent="*android:Preference.Material.CheckBoxPreference">
+        <item name="android:layout">@*android:layout/car_preference</item>
+    </style>
+    <style name="Preference.DeviceDefault.DialogPreference" parent="*android:Preference.Material.DialogPreference">
+        <item name="android:layout">@*android:layout/car_preference</item>
+    </style>
+    <style name="Preference.DeviceDefault.DialogPreference.EditTextPreference" parent="*android:Preference.Material.DialogPreference.EditTextPreference">
+        <item name="android:layout">@*android:layout/car_preference</item>
+    </style>
+    <style name="Preference.DeviceDefault.SwitchPreference" parent="*android:Preference.Material.SwitchPreference">
+        <item name="android:layout">@*android:layout/car_preference</item>
+    </style>
+
+    <!-- AlertDialog Style -->
+    <style name="AlertDialog.DeviceDefault" parent="*android:AlertDialog.Material">
+        <item name="android:layout">@*android:layout/car_alert_dialog</item>
+    </style>
+
+</resources>
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
new file mode 100644
index 0000000..09731bb
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/themes_device_defaults.xml
@@ -0,0 +1,263 @@
+<?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 is an override of frameworks/base/core/res/res/values/themes_device_defaults.xml
+    It is how the device default is changed to match the desired look for a car theme.
+-->
+<resources>
+    <style name="Theme.DeviceDefault" parent="*android:Theme.DeviceDefaultBase">
+        <!-- Text styles -->
+        <!-- TODO clean up -->
+
+        <item name="android:textAppearanceListItem">@*android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceListItemSmall">@*android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceListItemSecondary">@*android:style/TextAppearance.DeviceDefault.Small</item>
+
+        <item name="android:borderlessButtonStyle">@*android:style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+        <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+        <item name="android:buttonStyle">@*android:style/Widget.DeviceDefault.Button</item>
+
+
+        <item name="android:listPreferredItemHeightSmall">@*android:dimen/car_single_line_list_item_height</item>
+
+
+        <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+
+        <item name="android:actionBarSize">@*android:dimen/car_app_bar_height</item>
+
+        <!-- Color palette -->
+        <item name="android:colorBackgroundFloating">@color/floating_background_color</item>
+        <item name="android:statusBarColor">@android:color/black</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>
+        <item name="android:alertDialogTheme">@android:style/Theme.DeviceDefault.Dialog.Alert</item>
+    </style>
+
+    <style name="Theme.DeviceDefault.Dialog" parent="android:Theme.Material.Dialog">
+        <item name="android:textAppearanceLarge">@*android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceMedium">@*android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textAppearanceSmall">@*android:style/TextAppearance.DeviceDefault.Small</item>
+        <item name="android:textAppearanceLargeInverse">@*android:style/TextAppearance.DeviceDefault.Large.Inverse</item>
+        <item name="android:textAppearanceMediumInverse">@*android:style/TextAppearance.DeviceDefault.Medium.Inverse</item>
+        <item name="android:textAppearanceSmallInverse">@*android:style/TextAppearance.DeviceDefault.Small.Inverse</item>
+        <item name="android:textAppearanceListItem">@*android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceListItemSmall">@*android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceListItemSecondary">@*android:style/TextAppearance.DeviceDefault.Small</item>
+        <item name="android:textAppearanceButton">@*android:style/Widget.DeviceDefault.Button</item>
+        <item name="android:borderlessButtonStyle">@*android:style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+        <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+        <item name="android:buttonStyle">@*android:style/Widget.DeviceDefault.Button</item>
+        <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+        <item name="android:windowTitleStyle">?android:attr/textAppearanceLarge</item>
+        <!-- Color palette -->
+        <item name="android:colorButtonNormal">@color/btn_device_default_dark</item>
+    </style>
+
+    <style name="Theme.DeviceDefault.Dialog.NoActionBar" parent="android:Theme.DeviceDefault.Dialog">
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
+
+    <style name="Theme.DeviceDefault.Dialog.Alert" parent="android:Theme.Material.Dialog.Alert">
+        <item name="android:alertDialogTheme">?android:attr/alertDialogTheme</item>
+
+        <item name="android:textAppearanceLarge">@*android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceMedium">@*android:style/TextAppearance.DeviceDefault.Medium</item>
+        <item name="android:textAppearanceSmall">@*android:style/TextAppearance.DeviceDefault.Small</item>
+        <item name="android:textAppearanceButton">@*android:style/Widget.DeviceDefault.Button</item>
+        <item name="android:alertDialogStyle">@*android:style/AlertDialog.DeviceDefault</item>
+        <item name="android:borderlessButtonStyle">@*android:style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+        <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+        <item name="android:buttonStyle">@*android:style/Widget.DeviceDefault.Button</item>
+        <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+        <item name="android:textAppearanceListItem">@*android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceListItemSmall">@*android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceListItemSecondary">@*android:style/TextAppearance.DeviceDefault.Small</item>
+        <item name="android:windowTitleStyle">?android:attr/textAppearanceLarge</item>
+        <!-- Color palette -->
+        <item name="android:colorButtonNormal">@color/btn_device_default_dark</item>
+        <item name="android:background">@color/alert_dialog_background_color</item>
+    </style>
+
+    <style name="Theme.DeviceDefault.Settings.Dialog" parent="android:Theme.DeviceDefault.Dialog.Alert">
+    </style>
+
+    <!-- The light theme is defined to be the same as the default since currently there is only one
+        defined theme palette -->
+    <style name="Theme.DeviceDefault.Light" parent="android:Theme.DeviceDefault"/>
+    <style name="Theme.DeviceDefault.Light.Dialog" parent="android:Theme.DeviceDefault.Dialog"/>
+    <style name="Theme.DeviceDefault.Light.Dialog.Alert" parent="android:Theme.DeviceDefault.Dialog.Alert"/>
+    <style name="Theme.DeviceDefault.Light.Dialog.NoActionBar" parent="android:Theme.DeviceDefault.Dialog.NoActionBar"/>
+
+    <style name="Theme.DeviceDefault.Light.NoActionBar" parent="android:Theme.DeviceDefault.Light">
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
+    <style name="Theme.DeviceDefault.NoActionBar" parent="android:Theme.DeviceDefault">
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
+
+    <style name="Theme.DeviceDefault.InputMethod" parent="android:Theme.Material.InputMethod">
+        <!-- Color palette -->
+        <item name="android:colorAccent">@*android:color/accent_device_default_light</item>
+        <item name="android:colorBackground">@*android:color/primary_device_default_light</item>
+        <item name="android:listDivider">@*android:color/car_keyboard_divider_line</item>
+        <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+        <item name="android:textColorPrimary">@*android:color/car_keyboard_text_primary_color</item>
+        <item name="android:textColorSecondary">@*android:color/car_keyboard_text_secondary_color</item>
+    </style>
+
+    <style name="Theme.DeviceDefault.Settings" parent="android:Theme.DeviceDefault"/>
+    <style name="Theme.DeviceDefault.Settings.NoActionBar" parent="android:Theme.DeviceDefault.NoActionBar"/>
+
+    <style name="Theme.DeviceDefault.Light.DarkActionBar"  parent="android:Theme.DeviceDefault"/>
+    <!-- DeviceDefault theme for the default system theme.  -->
+    <style name="Theme.DeviceDefault.System" parent="android:Theme.DeviceDefault.Light.DarkActionBar" />
+
+    <!-- Theme used for the intent picker activity. -->
+    <style name="Theme.DeviceDefault.Resolver" parent="android:Theme.DeviceDefault">
+        <item name="android:windowEnterTransition">@empty</item>
+        <item name="android:windowExitTransition">@empty</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:backgroundDimEnabled">true</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:colorControlActivated">?*android:attr/colorControlHighlight</item>
+        <item name="android:listPreferredItemPaddingStart">?*android:attr/dialogPreferredPadding</item>
+        <item name="android:listPreferredItemPaddingEnd">?*android:attr/dialogPreferredPadding</item>
+
+        <!-- Dialog attributes -->
+        <item name="android:dialogCornerRadius">@*android:dimen/config_dialogCornerRadius</item>
+
+        <!-- Button styles -->
+        <item name="android:buttonCornerRadius">@*android:dimen/config_buttonCornerRadius</item>
+        <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+        <item name="android:borderlessButtonStyle">@*android:style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+        <item name="android:buttonStyle">@*android:style/Widget.DeviceDefault.Button</item>
+
+        <!-- Color palette -->
+        <item name="android:colorButtonNormal">@color/btn_device_default_dark</item>
+
+        <!-- Progress bar attributes -->
+        <item name="*android:colorProgressBackgroundNormal">@*android:color/config_progress_background_tint</item>
+        <item name="*android:progressBarCornerRadius">@*android:dimen/config_progressBarCornerRadius</item>
+
+        <!-- Toolbar attributes -->
+        <item name="android:toolbarStyle">@*android:style/Widget.DeviceDefault.Toolbar</item>
+
+        <item name="*android:toastFrameBackground">@*android:drawable/toast_frame</item>
+        <item name="android:textAppearanceListItem">@android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.DeviceDefault.Large</item>
+        <item name="android:textAppearanceListItemSecondary">@android:style/TextAppearance.DeviceDefault.Small</item>
+
+        <!-- Icon sizes -->
+        <item name="*android:iconfactoryIconSize">@*android:dimen/resolver_icon_size</item>
+        <item name="*android:iconfactoryBadgeSize">@*android:dimen/resolver_badge_size</item>
+    </style>
+
+
+    <!-- DeviceDefault theme for windows that want to have the user's selected wallpaper appear
+    behind them. -->
+    <style name="Theme.DeviceDefault.Wallpaper" parent="android:Theme.DeviceDefault">
+        <!-- Color palette -->
+
+        <!-- Dialog attributes -->
+        <item name="android:dialogCornerRadius">@*android:dimen/config_dialogCornerRadius</item>
+
+        <!-- Text styles -->
+        <item name="android:textAppearanceButton">@*android:style/TextAppearance.DeviceDefault.Widget.Button</item>
+
+        <!-- Button styles -->
+        <item name="android:buttonCornerRadius">@*android:dimen/config_buttonCornerRadius</item>
+        <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+
+        <!-- Progress bar attributes -->
+        <item name="*android:colorProgressBackgroundNormal">@*android:color/config_progress_background_tint</item>
+        <item name="*android:progressBarCornerRadius">@*android:dimen/config_progressBarCornerRadius</item>
+
+        <!-- Toolbar attributes -->
+        <item name="android:toolbarStyle">@*android:style/Widget.DeviceDefault.Toolbar</item>
+
+        <item name="android:windowBackground">@*android:color/transparent</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowShowWallpaper">true</item>
+    </style>
+
+    <!-- DeviceDefault theme for windows that want to have the user's selected wallpaper appear
+    behind them and without an action bar. -->
+    <style name="Theme.DeviceDefault.Wallpaper.NoTitleBar" parent="android:Theme.DeviceDefault.Wallpaper">
+        <!-- Color palette -->
+
+        <!-- Dialog attributes -->
+        <item name="android:dialogCornerRadius">@*android:dimen/config_dialogCornerRadius</item>
+
+        <!-- Text styles -->
+        <item name="android:textAppearanceButton">@*android:style/TextAppearance.DeviceDefault.Widget.Button</item>
+
+        <!-- Button styles -->
+        <item name="android:buttonCornerRadius">@*android:dimen/config_buttonCornerRadius</item>
+        <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+
+        <!-- Progress bar attributes -->
+        <item name="*android:colorProgressBackgroundNormal">@*android:color/config_progress_background_tint</item>
+        <item name="*android:progressBarCornerRadius">@*android:dimen/config_progressBarCornerRadius</item>
+
+        <!-- Toolbar attributes -->
+        <item name="android:toolbarStyle">@*android:style/Widget.DeviceDefault.Toolbar</item>
+
+        <item name="android:windowNoTitle">true</item>
+    </style>
+
+    <!-- DeviceDefault theme for panel windows. This removes all extraneous window decorations, so
+    you basically have an empty rectangle in which to place your content. It makes the window
+    floating, with a transparent background, and turns off dimming behind the window.
+    Used for Autofill screens.-->
+    <style name="Theme.DeviceDefault.Panel" parent="android:Theme.Material.Panel">
+        <!-- Color palette -->
+
+        <!-- Dialog attributes -->
+        <item name="android:dialogCornerRadius">@*android:dimen/config_dialogCornerRadius</item>
+
+        <!-- Text styles -->
+        <item name="android:textAppearanceButton">@*android:style/TextAppearance.DeviceDefault.Widget.Button</item>
+
+        <!-- Button styles -->
+        <item name="android:buttonCornerRadius">@*android:dimen/config_buttonCornerRadius</item>
+        <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+
+        <!-- Progress bar attributes -->
+        <item name="*android:colorProgressBackgroundNormal">@*android:color/config_progress_background_tint</item>
+        <item name="*android:progressBarCornerRadius">@*android:dimen/config_progressBarCornerRadius</item>
+
+        <!-- Toolbar attributes -->
+        <item name="android:toolbarStyle">@*android:style/Widget.DeviceDefault.Toolbar</item>
+
+        <!-- Hide action bar -->
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
+
+        <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+    </style>
+
+    <style name="Theme.DeviceDefault.Light.Panel" parent="android:Theme.DeviceDefault.Panel"/>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/Android.mk b/car_product/car_ui_portrait/rro/car-ui-customizations/Android.mk
new file mode 100644
index 0000000..96c6d30
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/Android.mk
@@ -0,0 +1,57 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+CAR_UI_RRO_SET_NAME := generated_caruiportrait_customization
+CAR_UI_RRO_MANIFEST_FILE := $(LOCAL_PATH)/AndroidManifest.xml
+CAR_UI_RESOURCE_DIR := $(LOCAL_PATH)/res
+CAR_UI_RRO_TARGETS := \
+    com.android.car.ui.paintbooth \
+    com.google.android.car.ui.paintbooth \
+    com.google.android.carui.ats \
+    com.android.car.rotaryplayground \
+    com.android.car.themeplayground \
+    com.android.car.carlauncher \
+    com.android.car.home \
+    com.android.car.media \
+    com.android.car.radio \
+    com.android.car.calendar \
+    com.android.car.companiondevicesupport \
+    com.android.car.systemupdater \
+    com.android.car.dialer \
+    com.android.car.linkviewer \
+    com.android.car.settings \
+    com.android.car.voicecontrol \
+    com.android.car.faceenroll \
+    com.android.car.developeroptions \
+    com.android.managedprovisioning \
+    com.android.settings.intelligence \
+    com.google.android.apps.automotive.inputmethod \
+    com.google.android.apps.automotive.inputmethod.dev \
+    com.google.android.apps.automotive.templates.host \
+    com.google.android.embedded.projection \
+    com.google.android.gms \
+    com.google.android.gsf \
+    com.google.android.packageinstaller \
+    com.google.android.permissioncontroller \
+    com.google.android.carassistant \
+    com.google.android.tts \
+    com.android.htmlviewer \
+    com.android.vending \
+
+include packages/apps/Car/libs/car-ui-lib/generate_rros.mk
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/AndroidManifest.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/AndroidManifest.xml
new file mode 100644
index 0000000..9d3a1a4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/AndroidManifest.xml
@@ -0,0 +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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="{{RRO_PACKAGE_NAME}}">
+    <application android:hasCode="false"/>
+    <overlay android:priority="10"
+             android:targetName="car-ui-lib"
+             android:targetPackage="{{TARGET_PACKAGE_NAME}}"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true"
+             android:requiredSystemPropertyName="ro.build.characteristics"
+             android:requiredSystemPropertyValue="automotive"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/README b/car_product/car_ui_portrait/rro/car-ui-customizations/README
new file mode 100644
index 0000000..1e14b27
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/README
@@ -0,0 +1,2 @@
+The values in this RRO are for modifying the car-ui-lib values and should be applied to all
+applications using car-ui-lib
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/product.mk b/car_product/car_ui_portrait/rro/car-ui-customizations/product.mk
new file mode 100644
index 0000000..a7c3808
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/product.mk
@@ -0,0 +1,51 @@
+#
+# 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.
+#
+
+# Inherit from this product to include the "Car Ui Portrait" RROs for CarUi
+# Include generated RROs
+PRODUCT_PACKAGES += \
+    generated_caruiportrait_customization-com-android-car-ui-paintbooth \
+    generated_caruiportrait_customization-com-google-android-car-ui-paintbooth \
+    generated_caruiportrait_customization-com-google-android-carui-ats \
+    generated_caruiportrait_customization-com-android-car-rotaryplayground \
+    generated_caruiportrait_customization-com-android-car-themeplayground \
+    generated_caruiportrait_customization-com-android-car-carlauncher \
+    generated_caruiportrait_customization-com-android-car-home \
+    generated_caruiportrait_customization-com-android-car-media \
+    generated_caruiportrait_customization-com-android-car-radio \
+    generated_caruiportrait_customization-com-android-car-calendar \
+    generated_caruiportrait_customization-com-android-car-companiondevicesupport \
+    generated_caruiportrait_customization-com-android-car-systemupdater \
+    generated_caruiportrait_customization-com-android-car-dialer \
+    generated_caruiportrait_customization-com-android-car-linkviewer \
+    generated_caruiportrait_customization-com-android-car-settings \
+    generated_caruiportrait_customization-com-android-car-voicecontrol \
+    generated_caruiportrait_customization-com-android-car-faceenroll \
+    generated_caruiportrait_customization-com-android-car-developeroptions \
+    generated_caruiportrait_customization-com-android-managedprovisioning \
+    generated_caruiportrait_customization-com-android-settings-intelligence \
+    generated_caruiportrait_customization-com-google-android-apps-automotive-inputmethod \
+    generated_caruiportrait_customization-com-google-android-apps-automotive-inputmethod-dev \
+    generated_caruiportrait_customization-com-google-android-apps-automotive-templates-host \
+    generated_caruiportrait_customization-com-google-android-embedded-projection \
+    generated_caruiportrait_customization-com-google-android-gms \
+    generated_caruiportrait_customization-com-google-android-gsf \
+    generated_caruiportrait_customization-com-google-android-packageinstaller \
+    generated_caruiportrait_customization-com-google-android-permissioncontroller \
+    generated_caruiportrait_customization-com-google-android-carassistant \
+    generated_caruiportrait_customization-com-google-android-tts \
+    generated_caruiportrait_customization-com-android-htmlviewer \
+    generated_caruiportrait_customization-com-android-vending
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
new file mode 100644
index 0000000..e87a692
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_primary.xml
@@ -0,0 +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.
+  -->
+<!-- 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/textColorPrimary"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          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
new file mode 100644
index 0000000..0f1fcb5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_secondary.xml
@@ -0,0 +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.
+  -->
+<!-- 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/textColorSecondary"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          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/color/car_ui_toolbar_tab_item_selector.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_toolbar_tab_item_selector.xml
new file mode 100644
index 0000000..02d4374
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_toolbar_tab_item_selector.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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/car_ui_text_color_primary" android:state_activated="true"/>
+    <item android:color="@color/car_ui_text_color_secondary"/>
+</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/drawable/car_ui_recyclerview_ic_down.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_down.xml
new file mode 100644
index 0000000..e2d1b93
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_down.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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M14.83,16.42L24,25.59l9.17,-9.17L36,19.25l-12,12 -12,-12z"
+        android:fillColor="#000000"/>
+</vector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_up.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_up.xml
new file mode 100644
index 0000000..c8cc84f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_up.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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M14.83,30.83L24,21.66l9.17,9.17L36,28 24,16 12,28z"
+        android:fillColor="#000000"/>
+</vector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_scrollbar_thumb.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_scrollbar_thumb.xml
new file mode 100644
index 0000000..54922cf
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_scrollbar_thumb.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">
+    <solid android:color="#99000000" />
+    <corners android:radius="100dp"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml
new file mode 100644
index 0000000..9b47736
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_divider.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.
+  ~
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <size android:width="16dp"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_background.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_background.xml
new file mode 100644
index 0000000..57ac917
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_background.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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <size
+        android:width="40dp"
+        android:height="40dp"/>
+    <solid android:color="@android:color/transparent"/>
+</shape>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
new file mode 100644
index 0000000..9ac2a1f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.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.
+  ~
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+        android:color="#27ffffff"
+        android:radius="48dp"/>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/layout/car_ui_alert_dialog_title_with_subtitle.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
new file mode 100644
index 0000000..81cfb5c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
@@ -0,0 +1,63 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/title_template"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <!-- Leave this view here so that we don't get any null pointer errors in the alert dialog
+         class. -->
+    <ImageView
+        android:id="@+id/car_ui_alert_icon"
+        android:layout_width="96dp"
+        android:layout_height="96dp"
+        android:layout_marginStart="10dp"
+        android:layout_marginTop="@dimen/alert_dialog_margin"
+        android:scaleType="fitCenter"
+        android:tint="@color/car_ui_text_color_primary"
+        android:visibility="gone"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/alert_dialog_margin"
+        android:layout_marginEnd="@dimen/alert_dialog_margin"
+        android:layout_marginTop="@dimen/alert_dialog_margin"
+        android:orientation="vertical">
+        <TextView
+            android:id="@+id/car_ui_alert_title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:textAppearance="@style/TextAppearance_CarUi_AlertDialog_Title" />
+
+        <TextView
+            android:id="@+id/car_ui_alert_subtitle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:textAppearance="@style/TextAppearance_CarUi_AlertDialog_Subtitle"/>
+    </LinearLayout>
+
+    <View
+        android:id="@+id/empty_space"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"/>
+</LinearLayout>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/attrs.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/attrs.xml
new file mode 100644
index 0000000..6b60f12
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/attrs.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>
+    <attr name="state_ux_restricted" format="boolean" />
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/dimens.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/dimens.xml
new file mode 100644
index 0000000..a3ce819
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/dimens.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>
+    <dimen name="car_ui_body1_size">32sp</dimen>
+    <dimen name="car_ui_body3_size">24sp</dimen>
+
+    <dimen name="alert_dialog_margin">36dp</dimen>
+</resources>
\ No newline at end of file
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
new file mode 100644
index 0000000..f44dbf0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/drawables.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: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
new file mode 100644
index 0000000..f154c0b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/styles.xml
@@ -0,0 +1,52 @@
+<?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="TextAppearance_CarUi_AlertDialog_Title" parent="android:TextAppearance.DeviceDefault">
+        <item name="android:textSize">32sp</item>
+    </style>
+    <style name="TextAppearance_CarUi_AlertDialog_Subtitle" parent="android:TextAppearance.DeviceDefault">
+        <item name="android:textSize">24sp</item>
+    </style>
+
+    <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
new file mode 100644
index 0000000..85dfc95
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/themes.xml
@@ -0,0 +1,18 @@
+<!--
+  ~ 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>
+
+</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
new file mode 100644
index 0000000..0049818
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/xml/overlays.xml
@@ -0,0 +1,53 @@
+<?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/car_ui_alert_dialog_title_with_subtitle" value="@layout/car_ui_alert_dialog_title_with_subtitle"/>
+
+    <item target="id/car_ui_alert_icon" value="@id/car_ui_alert_icon"/>
+    <item target="id/car_ui_alert_title" value="@id/car_ui_alert_title"/>
+    <item target="id/car_ui_alert_subtitle" value="@id/car_ui_alert_subtitle"/>
+
+    <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" />
+    <item target="drawable/car_ui_recyclerview_scrollbar_thumb" value="@drawable/car_ui_recyclerview_scrollbar_thumb" />
+    <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/Android.mk b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/Android.mk
new file mode 100644
index 0000000..1b06791
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/Android.mk
@@ -0,0 +1,28 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+CAR_UI_RRO_SET_NAME := generated_caruiportrait_toolbar
+CAR_UI_RRO_MANIFEST_FILE := $(LOCAL_PATH)/AndroidManifest.xml
+CAR_UI_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+CAR_UI_RRO_TARGETS := \
+    com.android.car.media \
+    com.android.car.dialer \
+
+include packages/apps/Car/libs/car-ui-lib/generate_rros.mk
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/AndroidManifest.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/AndroidManifest.xml
new file mode 100644
index 0000000..9d3a1a4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/AndroidManifest.xml
@@ -0,0 +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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="{{RRO_PACKAGE_NAME}}">
+    <application android:hasCode="false"/>
+    <overlay android:priority="10"
+             android:targetName="car-ui-lib"
+             android:targetPackage="{{TARGET_PACKAGE_NAME}}"
+             android:resourcesMap="@xml/overlays"
+             android:isStatic="true"
+             android:requiredSystemPropertyName="ro.build.characteristics"
+             android:requiredSystemPropertyValue="automotive"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/README b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/README
new file mode 100644
index 0000000..e199f7b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/README
@@ -0,0 +1,2 @@
+The values in this RRO are to change the placement of the car-ui toolbar as such it currently
+is only targeting a limited set of applications.
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/product.mk b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/product.mk
new file mode 100644
index 0000000..7b7cccd
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/product.mk
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+
+# Inherit from this product to include the "Car Ui Portrait" RROs for CarUi
+# Include generated RROs
+PRODUCT_PACKAGES += \
+    generated_caruiportrait_toolbar-com-android-car-media \
+    generated_caruiportrait_toolbar-com-android-car-dialer \
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_primary.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_primary.xml
new file mode 100644
index 0000000..860f219
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_primary.xml
@@ -0,0 +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.
+-->
+<!-- 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"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item android:color="?android:attr/colorForeground"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_secondary.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_secondary.xml
new file mode 100644
index 0000000..f99fc86
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_secondary.xml
@@ -0,0 +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.
+-->
+<!-- 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"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item android:color="?android:attr/colorForeground"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_toolbar_tab_item_selector.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_toolbar_tab_item_selector.xml
new file mode 100644
index 0000000..02d4374
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_toolbar_tab_item_selector.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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/car_ui_text_color_primary" android:state_activated="true"/>
+    <item android:color="@color/car_ui_text_color_secondary"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/tab_side_indicator_color.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/tab_side_indicator_color.xml
new file mode 100644
index 0000000..0cf2a1a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/tab_side_indicator_color.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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="#52CCB0" android:state_activated="true"/>
+    <item android:color="@android:color/transparent"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml
new file mode 100644
index 0000000..9b47736
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_divider.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.
+  ~
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <size android:width="16dp"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
new file mode 100644
index 0000000..9ac2a1f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.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.
+  ~
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+        android:color="#27ffffff"
+        android:radius="48dp"/>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/tab_background.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/tab_background.xml
new file mode 100644
index 0000000..ffbeb18
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/tab_background.xml
@@ -0,0 +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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_activated="true">
+        <shape android:shape="rectangle">
+            <solid android:color="@color/tab_background_color"/>
+        </shape>
+    </item>
+    <item android:state_activated="false">
+        <shape android:shape="rectangle">
+            <solid android:color="@android:color/transparent"/>
+        </shape>
+    </item>
+</selector>
\ No newline at end of file
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
new file mode 100644
index 0000000..642ea29
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_base_layout_toolbar.xml
@@ -0,0 +1,201 @@
+<?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 is for the two-row version of the toolbar -->
+<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"
+    android:tag="CarUiBaseLayoutToolbar">
+
+    <!-- When not in touch mode, if we clear focus in current window, Android will re-focus the
+         first focusable view in the window automatically. Adding a FocusParkingView to the window
+         can fix this issue, because it can take focus, and it is transparent and its default focus
+         highlight is disabled, so it's invisible to the user no matter whether it's focused or not.
+         -->
+    <com.android.car.ui.FocusParkingView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <FrameLayout
+        android:id="@+id/car_ui_base_layout_content_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingStart="24dp"
+        android:paddingEnd="24dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toEndOf="@id/left_part_of_toolbar_focus_area"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+    <com.android.car.ui.FocusArea
+        android:id="@+id/top_part_of_toolbar_focus_area"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="90dp"
+            android:background="?android:attr/colorBackground"
+            android:tag="car_ui_top_inset"
+            app:layout_constraintTop_toTopOf="parent">
+            <com.android.car.ui.baselayout.ClickBlockingView
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"/>
+
+            <FrameLayout
+                android:id="@+id/car_ui_toolbar_nav_icon_container"
+                android:layout_width="90dp"
+                android:layout_height="0dp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent">
+
+                <ImageView
+                    android:id="@+id/car_ui_toolbar_nav_icon"
+                    android:layout_width="@dimen/car_ui_toolbar_nav_icon_size"
+                    android:layout_height="@dimen/car_ui_toolbar_nav_icon_size"
+                    android:layout_gravity="center"
+                    android:scaleType="fitXY"
+                    android:background="@drawable/car_ui_toolbar_menu_item_icon_ripple"
+                    android:tint="?android:attr/textColorPrimary"/>
+
+                <ImageView
+                    android:id="@+id/car_ui_toolbar_logo"
+                    android:layout_width="@dimen/car_ui_toolbar_logo_size"
+                    android:layout_height="@dimen/car_ui_toolbar_logo_size"
+                    android:layout_gravity="center"
+                    android:scaleType="fitXY" />
+            </FrameLayout>
+
+            <FrameLayout
+                android:id="@+id/car_ui_toolbar_title_logo_container"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="24dp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toStartOf="@id/car_ui_toolbar_title_container"
+                app:layout_constraintHorizontal_chainStyle="packed">
+
+                <ImageView
+                    android:id="@+id/car_ui_toolbar_title_logo"
+                    android:layout_width="@dimen/car_ui_toolbar_logo_size"
+                    android:layout_height="@dimen/car_ui_toolbar_logo_size"
+                    android:layout_gravity="center"
+                    android:scaleType="fitXY" />
+            </FrameLayout>
+
+            <LinearLayout android:layout_height="wrap_content"
+                          android:layout_width="wrap_content"
+                          android:id="@+id/car_ui_toolbar_title_container"
+                          android:orientation="vertical"
+                          android:layout_marginStart="16dp"
+                          app:layout_goneMarginStart="0dp"
+                          app:layout_constraintBottom_toBottomOf="parent"
+                          app:layout_constraintTop_toTopOf="parent"
+                          app:layout_constraintEnd_toEndOf="parent"
+                          app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_title_logo_container">
+                <TextView android:id="@+id/car_ui_toolbar_title"
+                          android:layout_width="wrap_content"
+                          android:layout_height="wrap_content"
+                          android:singleLine="true"
+                          android:textAlignment="viewStart"
+                          android:textAppearance="@style/TextAppearance.CarUi.Widget.Toolbar.Title"/>
+                <TextView android:id="@+id/car_ui_toolbar_subtitle"
+                          android:layout_width="wrap_content"
+                          android:layout_height="wrap_content"
+                          android:visibility="gone"
+                          android:textAlignment="viewStart"
+                          android:textAppearance="?android:attr/textAppearanceSmall"/>
+            </LinearLayout>
+
+            <FrameLayout
+                android:id="@+id/car_ui_toolbar_search_view_container"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"
+                app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_nav_icon_container"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <LinearLayout
+                android:id="@+id/car_ui_toolbar_menu_items_container"
+                android:divider="@drawable/car_ui_toolbar_menu_item_divider"
+                android:showDividers="beginning|middle|end"
+                android:layout_width="wrap_content"
+                android:layout_height="0dp"
+                android:orientation="horizontal"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <ProgressBar
+                android:id="@+id/car_ui_toolbar_progress_bar"
+                style="@android:style/Widget.DeviceDefault.ProgressBar.Horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:indeterminate="true"
+                android:visibility="gone"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent" />
+
+            <!-- Hairline across bottom of toolbar -->
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="2dp"
+                android:background="@color/divider_color"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </com.android.car.ui.FocusArea>
+
+    <com.android.car.ui.FocusArea
+        android:id="@+id/left_part_of_toolbar_focus_area"
+        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">
+
+        <com.android.car.ui.toolbar.TabLayout
+            android:id="@+id/car_ui_toolbar_tabs"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:orientation="vertical"/>
+    </com.android.car.ui.FocusArea>
+
+    <!-- Hairline to the right of the tabs -->
+    <View
+        android:layout_width="2dp"
+        android:layout_height="0dp"
+        android:background="@color/divider_color"
+        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-toolbar-customizations/res/layout/car_ui_toolbar_tab_item.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_toolbar_tab_item.xml
new file mode 100644
index 0000000..63ac2b4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_toolbar_tab_item.xml
@@ -0,0 +1,56 @@
+<?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="320dp"
+    android:layout_height="96dp"
+    android:background="@drawable/tab_background">
+
+    <View
+        android:layout_width="8dp"
+        android:layout_height="match_parent"
+        android:background="@color/tab_side_indicator_color"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <ImageView
+        android:id="@+id/car_ui_toolbar_tab_item_icon"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
+        android:scaleType="fitCenter"
+        android:tint="@color/car_ui_toolbar_tab_item_selector"
+        android:tintMode="src_in"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_toolbar_tab_item_text"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+    <TextView
+        android:id="@+id/car_ui_toolbar_tab_item_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textSize="28sp"
+        android:singleLine="true"
+        app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_tab_item_icon"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-night/colors.xml
new file mode 100644
index 0000000..52db35b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-night/colors.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:android="http://schemas.android.com/apk/res/android">
+    <color name="tab_background_color">#282A2D</color>
+    <color name="divider_color">#2e3134</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-port/values.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-port/values.xml
new file mode 100644
index 0000000..299d726
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-port/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_toolbar_tab_flexible_layout">false</bool>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/attrs.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/attrs.xml
new file mode 100644
index 0000000..e06d40a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/attrs.xml
@@ -0,0 +1,78 @@
+<?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>
+    <attr name="layout_constraintGuide_begin" format="dimension"/>
+    <attr name="layout_constraintGuide_end" format="dimension"/>
+    <attr name="layout_constraintGuide_percent" format="float"/>
+
+    <attr name="layout_constraintLeft_toLeftOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintLeft_toRightOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintRight_toLeftOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintRight_toRightOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintTop_toTopOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintTop_toBottomOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintBottom_toTopOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintBottom_toBottomOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintBaseline_toBaselineOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintStart_toEndOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintStart_toStartOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintEnd_toStartOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+    <attr name="layout_constraintEnd_toEndOf" format="reference|enum">
+        <enum name="parent" value="0"/>
+    </attr>
+
+    <attr name="layout_constraintHorizontal_bias" format="float"/>
+    <attr name="layout_constraintVertical_bias" format="float"/>
+
+    <attr name="layout_goneMarginLeft" format="dimension"/>
+    <attr name="layout_goneMarginTop" format="dimension"/>
+    <attr name="layout_goneMarginRight" format="dimension"/>
+    <attr name="layout_goneMarginBottom" format="dimension"/>
+    <attr name="layout_goneMarginStart" format="dimension"/>
+    <attr name="layout_goneMarginEnd" format="dimension"/>
+
+    <attr name="layout_constraintHorizontal_chainStyle" format="enum">
+        <enum name="spread" value="0"/>
+        <enum name="spread_inside" value="1"/>
+        <enum name="packed" value="2"/>
+    </attr>
+    <attr name="state_ux_restricted" format="boolean" />
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/bools.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/bools.xml
new file mode 100644
index 0000000..c502242
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/bools.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.
+  -->
+
+<resources>
+
+    <bool name="car_ui_toolbar_tabs_on_second_row">true</bool>
+
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/colors.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/colors.xml
new file mode 100644
index 0000000..8c7d1da
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/colors.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:android="http://schemas.android.com/apk/res/android">
+    <color name="tab_background_color">#E8EAED</color>
+    <color name="divider_color">#E8EAED</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/themes.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/themes.xml
new file mode 100644
index 0000000..62b5c1b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/themes.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ 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>
+    <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-toolbar-customizations/res/values/values.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/values.xml
new file mode 100644
index 0000000..c8f85cf
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/values.xml
@@ -0,0 +1,24 @@
+<?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_toolbar_logo_fills_nav_icon_space">false</bool>
+    <bool name="car_ui_toolbar_tab_flexible_layout">false</bool>
+    <bool name="car_ui_scrollbar_enable">false</bool>
+
+    <dimen name="car_ui_toolbar_logo_size">44dp</dimen>
+    <dimen name="car_ui_toolbar_nav_icon_size">44dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/xml/overlays.xml
new file mode 100644
index 0000000..88772fb
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/xml/overlays.xml
@@ -0,0 +1,65 @@
+<?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/car_ui_base_layout_toolbar" value="@layout/car_ui_base_layout_toolbar"/>
+    <item target="layout/car_ui_toolbar_tab_item" value="@layout/car_ui_toolbar_tab_item"/>
+
+    <item target="bool/car_ui_toolbar_logo_fills_nav_icon_space" value="@bool/car_ui_toolbar_logo_fills_nav_icon_space" />
+    <item target="bool/car_ui_toolbar_tab_flexible_layout" value="@bool/car_ui_toolbar_tab_flexible_layout" />
+    <item target="bool/car_ui_scrollbar_enable" value="@bool/car_ui_scrollbar_enable" />
+    <item target="bool/car_ui_toolbar_tabs_on_second_row" value="@bool/car_ui_toolbar_tabs_on_second_row" />
+
+    <item target="id/car_ui_toolbar_nav_icon_container" value="@id/car_ui_toolbar_nav_icon_container" />
+    <item target="id/car_ui_toolbar_nav_icon" value="@id/car_ui_toolbar_nav_icon" />
+    <item target="id/car_ui_toolbar_logo" value="@id/car_ui_toolbar_logo" />
+    <item target="id/car_ui_toolbar_title_logo_container" value="@id/car_ui_toolbar_title_logo_container" />
+    <item target="id/car_ui_toolbar_title_logo" value="@id/car_ui_toolbar_title_logo" />
+    <item target="id/car_ui_toolbar_title" value="@id/car_ui_toolbar_title" />
+    <item target="id/car_ui_toolbar_title_container" value="@id/car_ui_toolbar_title_container" />
+    <item target="id/car_ui_toolbar_subtitle" value="@id/car_ui_toolbar_subtitle" />
+    <item target="id/car_ui_toolbar_tabs" value="@id/car_ui_toolbar_tabs" />
+    <item target="id/car_ui_toolbar_menu_items_container" value="@id/car_ui_toolbar_menu_items_container" />
+    <item target="id/car_ui_toolbar_search_view_container" value="@id/car_ui_toolbar_search_view_container" />
+    <item target="id/car_ui_toolbar_progress_bar" value="@id/car_ui_toolbar_progress_bar" />
+    <item target="id/car_ui_base_layout_content_container" value="@id/car_ui_base_layout_content_container" />
+    <item target="id/car_ui_toolbar_tab_item_icon" value="@id/car_ui_toolbar_tab_item_icon" />
+    <item target="id/car_ui_toolbar_tab_item_text" value="@id/car_ui_toolbar_tab_item_text" />
+
+    <item target="attr/layout_constraintGuide_begin" value="@attr/layout_constraintGuide_begin"/>
+    <item target="attr/layout_constraintGuide_end" value="@attr/layout_constraintGuide_end"/>
+    <item target="attr/layout_constraintStart_toStartOf" value="@attr/layout_constraintStart_toStartOf"/>
+    <item target="attr/layout_constraintStart_toEndOf" value="@attr/layout_constraintStart_toEndOf"/>
+    <item target="attr/layout_constraintEnd_toStartOf" value="@attr/layout_constraintEnd_toStartOf"/>
+    <item target="attr/layout_constraintEnd_toEndOf" value="@attr/layout_constraintEnd_toEndOf"/>
+    <item target="attr/layout_constraintLeft_toLeftOf" value="@attr/layout_constraintLeft_toLeftOf"/>
+    <item target="attr/layout_constraintLeft_toRightOf" value="@attr/layout_constraintLeft_toRightOf"/>
+    <item target="attr/layout_constraintRight_toLeftOf" value="@attr/layout_constraintRight_toLeftOf"/>
+    <item target="attr/layout_constraintRight_toRightOf" value="@attr/layout_constraintRight_toRightOf"/>
+    <item target="attr/layout_constraintTop_toTopOf" value="@attr/layout_constraintTop_toTopOf"/>
+    <item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf"/>
+    <item target="attr/layout_constraintBottom_toTopOf" value="@attr/layout_constraintBottom_toTopOf"/>
+    <item target="attr/layout_constraintBottom_toBottomOf" value="@attr/layout_constraintBottom_toBottomOf"/>
+    <item target="attr/layout_constraintHorizontal_bias" value="@attr/layout_constraintHorizontal_bias"/>
+    <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginLeft"/>
+    <item target="attr/layout_goneMarginRight" value="@attr/layout_goneMarginRight"/>
+    <item target="attr/layout_goneMarginTop" value="@attr/layout_goneMarginTop"/>
+    <item target="attr/layout_goneMarginBottom" value="@attr/layout_goneMarginBottom"/>
+    <item target="attr/layout_goneMarginStart" value="@attr/layout_goneMarginStart"/>
+    <item target="attr/layout_goneMarginEnd" value="@attr/layout_goneMarginEnd"/>
+    <item target="attr/layout_constraintHorizontal_chainStyle" value="@attr/layout_constraintHorizontal_chainStyle"/>
+    <item target="attr/state_ux_restricted" value="@attr/state_ux_restricted"/>
+</overlay>
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
new file mode 100644
index 0000000..5b06ed2
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car_ui_portrait_rro.mk
@@ -0,0 +1,35 @@
+#
+# 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.
+#
+
+$(call inherit-product, packages/services/Car/car_product/car_ui_portrait/rro/car-ui-customizations/product.mk)
+$(call inherit-product, packages/services/Car/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/product.mk)
+
+# All RROs to be included in car_ui_portrait builds.
+PRODUCT_PACKAGES += \
+    CarEvsCameraPreviewAppRRO \
+    CarUiPortraitDialerRRO \
+    CarUiPortraitSettingsRRO \
+    CarUiPortraitMediaRRO \
+    CarUiPortraitLauncherRRO \
+    CarUiPortraitNotificationRRO \
+    CarUiPortraitCarServiceRRO \
+    CarUiPortraitFrameworkResRRO \
+    CarUiPortraitFrameworkResRROTest
+
+ifneq ($(INCLUDE_SEAHAWK_ONLY_RROS),)
+PRODUCT_PACKAGES += \
+    CarUiPortraitSettingsProviderRRO
+endif
diff --git a/car_product/car_ui_portrait/rro/common-res/res/color/car_ui_text_color_primary.xml b/car_product/car_ui_portrait/rro/common-res/res/color/car_ui_text_color_primary.xml
new file mode 100644
index 0000000..860f219
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/common-res/res/color/car_ui_text_color_primary.xml
@@ -0,0 +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.
+-->
+<!-- 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"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item android:color="?android:attr/colorForeground"/>
+</selector>
diff --git a/car_product/car_ui_portrait/tools/export_emulator.py b/car_product/car_ui_portrait/tools/export_emulator.py
new file mode 100755
index 0000000..aa75f3a
--- /dev/null
+++ b/car_product/car_ui_portrait/tools/export_emulator.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+
+import subprocess
+import os
+import sys
+from shutil import copy2, copytree, rmtree
+from argparse import ArgumentParser as AP
+
+# Mostly adapted from https://cs.android.com/android/platform/superproject/+/master:device/generic/car/tools/run_local_avd.sh
+
+def fromTop(path):
+    return os.path.join(os.environ['ANDROID_BUILD_TOP'], path)
+
+def fromProductOut(path):
+    return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], path)
+
+def copyImages(outputDir, abi):
+    outputDir = os.path.join(outputDir, abi)
+    os.mkdir(outputDir)
+
+    try:
+        copy2(fromProductOut('system-qemu.img'), os.path.join(outputDir, 'system.img'))
+        copy2(fromProductOut('vendor-qemu.img'), os.path.join(outputDir, 'vendor.img'))
+        if os.path.isfile(fromProductOut('kernel-ranchu-64')):
+            copy2(fromProductOut('kernel-ranchu-64'), outputDir)
+        else:
+            copy2(fromProductOut('kernel-ranchu'), outputDir)
+        copy2(fromProductOut('ramdisk-qemu.img'), os.path.join(outputDir, 'ramdisk.img'))
+        copy2(fromProductOut('encryptionkey.img'), outputDir)
+        # take prebuilt userdata.img
+        # Ref: https://cs.android.com/android/platform/superproject/+/master:development/build/sdk.atree?q=userdata.img&ss=android%2Fplatform%2Fsuperproject:development%2Fbuild%2F
+        copy2(fromTop('device/generic/goldfish/data/etc/userdata.img'), outputDir)
+        copytree(fromProductOut('data'), os.path.join(outputDir, 'data'), dirs_exist_ok=True)
+        copy2(fromProductOut('system/build.prop'), os.path.join(outputDir, 'build.prop'))
+        copy2(fromProductOut('VerifiedBootParams.textproto'), outputDir)
+        copy2(fromProductOut('config.ini'), outputDir)
+        copy2(fromProductOut('advancedFeatures.ini'), outputDir)
+    except FileNotFoundError as f:
+        print("File not found: "+f.filename+", did you build android first?")
+        sys.exit(1)
+
+def readScreenDimens(configini):
+    width = 1080
+    height = 1920
+    density = 160
+    with open(configini, 'r') as f:
+        for line in f.readlines():
+            parts = line.split(' = ')
+            if len(parts) != 2:
+                continue
+            if parts[0] == 'hw.lcd.width':
+                width = parts[1]
+            if parts[0] == 'hw.lcd.height':
+                height = parts[1]
+    return (width, height, density)
+
+def buildAVD(outputDir, abi):
+    os.makedirs(os.path.join(outputDir, '.android/avd/my_car_avd.avd/'), exist_ok=True)
+    with open(os.path.join(outputDir, '.android/avd/my_car_avd.ini'), 'w') as f:
+        f.write('avd.ini.encoding=UTF-8\n')
+        f.write('path=required_but_we_want_to_use_path.rel_instead\n')
+        f.write('path.rel=avd/my_car_avd.avd\n')
+
+    width, height, density = readScreenDimens(fromProductOut('config.ini'))
+
+    with open(os.path.join(outputDir, '.android/avd/my_car_avd.avd/config.ini'), 'w') as f:
+        f.write(f'''
+image.sysdir.1 = unused_because_passing_-sysdir_to_emulator
+hw.lcd.density = {density}
+hw.lcd.width = {width}
+hw.lcd.height = {height}
+AvdId = my_car_avd
+avd.ini.displayname = my_car_avd
+hw.ramSize = 3584
+abi.type = {abi}
+
+tag.display = Automotive
+tag.id = android-automotive
+hw.device.manufacturer = google
+hw.device.name = hawk
+avd.ini.encoding = UTF-8
+disk.dataPartition.size = 6442450944
+fastboot.chosenSnapshotFile =
+fastboot.forceChosenSnapshotBoot = no
+fastboot.forceColdBoot = no
+fastboot.forceFastBoot = yes
+hw.accelerometer = no
+hw.arc = false
+hw.audioInput = yes
+hw.battery = no
+hw.camera.back = None
+hw.camera.front = None
+hw.cpu.arch = x86_64
+hw.cpu.ncore = 4
+hw.dPad = no
+hw.device.hash2 = MD5:1fdb01985c7b4d7c19ec309cc238b0f9
+hw.gps = yes
+hw.gpu.enabled = yes
+hw.gpu.mode = auto
+hw.initialOrientation = landscape
+hw.keyboard = yes
+hw.keyboard.charmap = qwerty2
+hw.keyboard.lid = false
+hw.mainKeys = no
+hw.sdCard = no
+hw.sensors.orientation = no
+hw.sensors.proximity = no
+hw.trackBall = no
+runtime.network.latency = none
+runtime.network.speed = full
+''')
+
+def genStartScript(outputDir):
+    filepath = os.path.join(outputDir, 'start_emu.sh')
+    with open(os.open(filepath, os.O_CREAT | os.O_WRONLY, 0o750), 'w') as f:
+        f.write(f'''
+# This file is auto-generated from export_emulator.py
+OS="$(uname -s)"
+if [[ $OS == "Linux" ]]; then
+    DEFAULT_ANDROID_SDK_ROOT="$HOME/Android/Sdk"
+elif [[ $OS == "Darwin" ]]; then
+    DEFAULT_ANDROID_SDK_ROOT="/Users/$USER/Library/Android/sdk"
+else
+    echo Sorry, this does not work on $OS
+    exit
+fi
+if [[ -z $ANDROID_SDK_ROOT ]]; then
+    ANDROID_SDK_ROOT="$DEFAULT_ANDROID_SDK_ROOT"
+fi
+if ! [[ -d $ANDROID_SDK_ROOT ]]; then
+    echo Could not find android SDK root. Did you install an SDK with android studio?
+    exit
+fi
+
+# TODO: this ANDROID_EMULATOR_HOME may need to not be changed.
+# we had to change it so we could find the avd by a relative path,
+# but changing it means makes it give an "emulator is out of date"
+# warning
+
+# TODO: You shouldn't need to pass -sysdir, it should be specified
+# in the avd ini file. But I couldn't figure out how to make that work
+# with a relative path.
+
+ANDROID_EMULATOR_HOME=$(dirname $0)/.android \
+ANDROID_AVD_HOME=.android/avd \
+ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT \
+$ANDROID_SDK_ROOT/emulator/emulator \
+-avd my_car_avd -sysdir x86_64 $@
+''')
+
+
+def main():
+    parser = AP(description="Export the current build as a sharable emulator")
+    parser.add_argument('-o', '--output', default="/tmp/exported_emulator",
+                        help='Output folder. Defaults to /tmp/exported_emulator. Will wipe any existing contents!')
+    args = parser.parse_args()
+
+    if 'ANDROID_BUILD_TOP' not in os.environ or 'ANDROID_PRODUCT_OUT' not in os.environ:
+        print("Please run lunch first")
+        sys.exit(1)
+
+    if os.path.isfile(args.output):
+        print("Something already exists at "+args.output)
+        sys.exit(1)
+
+    if not os.path.isdir(os.path.dirname(args.output)):
+        print("Parent directory of "+args.output+" must already exist")
+        sys.exit(1)
+
+    rmtree(args.output, ignore_errors=True)
+    os.mkdir(args.output)
+
+    copyImages(args.output, 'x86_64')
+    buildAVD(args.output, 'x86_64')
+    genStartScript(args.output)
+    print("Done. Exported to "+args.output)
+
+
+if __name__ == "__main__":
+    main()
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-af/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-af/strings.xml
index abf8fd2..6bd87c4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-af/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-af/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Bestuurder"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"kry benaderde ligging net op die voorgrond"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Aktiveer mikrofoon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-am/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-am/strings.xml
index 0f190c4..f73000b 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-am/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-am/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ነጂ"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ከፊት ለፊት ብቻ ግምታዊ አካባቢን ድረስ"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"ማይክሮፎንን አንቃ"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ar/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ar/strings.xml
index aa51d5d..5efe621 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ar/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ar/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"السائق"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"الوصول إلى الموقع الجغرافي التقريبي في الواجهة الأمامية فقط"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"تشغيل الميكروفون"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-as/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-as/strings.xml
index ea877d4..7b0fa92 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-as/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-as/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"চালক"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"কেৱল অগ্ৰভূমিত আনুমানিক অৱস্থান এক্সেছ কৰক"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"মাইক্ৰ’ফ’ন সক্ষম কৰক।"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-az/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-az/strings.xml
index 3e2ccc1..65ec685 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-az/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-az/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Sürücü"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"yalnız ön planda təqribi məkana daxil olun"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Mikrofonu aktiv edin"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-b+sr+Latn/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-b+sr+Latn/strings.xml
index a20c58e..346f0e2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-b+sr+Latn/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-b+sr+Latn/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Vozač"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"pristup približnoj lokaciji samo u prvom planu"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Omogući mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-be/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-be/strings.xml
index d6e8de4..dc483db 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-be/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-be/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Вадзіцель"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"доступ да прыблізнага месцазнаходжання толькі ў асноўным рэжыме"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Уключыць мікрафон"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-bg/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-bg/strings.xml
index 7a2fd23..9842971 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-bg/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-bg/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Шофьор"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"достъп до приблизителното местоположение само на преден план"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Активиране на микрофона"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-bn/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-bn/strings.xml
index e9725c8..22ee318 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-bn/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-bn/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ড্রাইভার"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"শুধুমাত্র অ্যাপটি খোলা থাকলে আপনার আনুমানিক লোকেশন অ্যাক্সেস করা"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"মাইক্রোফোন চালু করুন"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-bs/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-bs/strings.xml
index a20c58e..346f0e2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-bs/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-bs/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Vozač"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"pristup približnoj lokaciji samo u prvom planu"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Omogući mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ca/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ca/strings.xml
index 8d24e22..e1a2044 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ca/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ca/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Conductor"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"accedeix a la ubicació aproximada només en primer pla"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Activa el micròfon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-cs/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-cs/strings.xml
index 2432df2..ce005f1 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-cs/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-cs/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Řidič"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"přístup k přibližné poloze jen na popředí"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Aktivovat mikrofon"</string>
 </resources>
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 731f251..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
@@ -19,4 +19,5 @@
     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 lokation i forgrunden"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Aktivér mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-de/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-de/strings.xml
index 9febde6..ba314a7 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-de/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-de/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Fahrer"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"Nur bei Ausführung im Vordergrund auf den ungefähren Standort zugreifen"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Mikrofon aktivieren"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-el/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-el/strings.xml
index fcfe739..4eca60d 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-el/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-el/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Οδηγός"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"πρόσβαση στην κατά προσέγγιση τοποθεσία μόνο στο προσκήνιο"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Ενεργοποίηση μικροφώνου"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rAU/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rAU/strings.xml
index 85c4908..91594b0 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rAU/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rAU/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Driver"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Enable microphone"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rCA/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rCA/strings.xml
index 85c4908..91594b0 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rCA/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rCA/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Driver"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Enable microphone"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rGB/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rGB/strings.xml
index 85c4908..91594b0 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rGB/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rGB/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Driver"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Enable microphone"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rIN/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rIN/strings.xml
index 85c4908..91594b0 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rIN/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rIN/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Driver"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Enable microphone"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rXC/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rXC/strings.xml
index 7778984..3e208b3 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rXC/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rXC/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‏‎‏‏‎‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‎‎‏‏‎‎‏‎‏‏‏‎‎‎‏‏‎‏‏‎‏‏‎‏‎‎‎Driver‎‏‎‎‏‎"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‏‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‏‎‎‎‎‏‎‏‎‏‎access approximate location only in the foreground‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‎‎‎‏‏‎‏‏‎‎‎‏‏‏‏‎‎‏‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‎‏‎‏‏‎‏‎‎‏‎‏‏‏‏‎‎‏‏‏‏‏‎Enable Microphone‎‏‎‎‏‎"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-es-rUS/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-es-rUS/strings.xml
index 5666331..ced0bde 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-es-rUS/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-es-rUS/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Conductor"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"acceder a la ubicación aproximada solo en primer plano"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Habilitar micrófono"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-es/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-es/strings.xml
index 3e3ad48..af83a18 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-es/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-es/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Conductor"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"acceder a la ubicación aproximada solo al estar en primer plano"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Habilitar micrófono"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-et/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-et/strings.xml
index 27ae02b..1876c65 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-et/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-et/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Sõitja"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"juurdepääs ligikaudsele asukohale ainult esiplaanil"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Luba mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-eu/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-eu/strings.xml
index f7e62cf..0e06b76 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-eu/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-eu/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Gidaria"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"atzitu gutxi gorabeherako kokapena aurreko planoan bakarrik"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Gaitu mikrofonoa"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-fa/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-fa/strings.xml
index 4d257d6..4c9f73a 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-fa/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-fa/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"راننده"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"دسترسی به مکان تقریبی فقط در پیش‌زمینه"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"فعال کردن میکروفن"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-fi/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-fi/strings.xml
index b2d5135..ce5daf3 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-fi/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-fi/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Kuljettaja"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"käyttää likimääräistä sijaintia vain etualalla"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Laita mikrofoni päälle"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-fr-rCA/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-fr-rCA/strings.xml
index 2ac4be7..eb83b85 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-fr-rCA/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-fr-rCA/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Conducteur"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"accéder à votre position approximative seulement en avant-plan"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Activer le microphone"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-fr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-fr/strings.xml
index 81af986..02b7a24 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-fr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-fr/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Conducteur"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"accéder à la position approximative au premier plan uniquement"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Activer le micro"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-gl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-gl/strings.xml
index 3754442..30b25d4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-gl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-gl/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Condutor"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"acceder á localización aproximada só en primeiro plano"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Activar micrófono"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-gu/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-gu/strings.xml
index 5c6ff84..e157fd3 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-gu/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-gu/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ડ્રાઇવર"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ફૉરગ્રાઉન્ડમાં ફક્ત અંદાજિત સ્થાન ઍક્સેસ કરો"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"માઇક્રોફોન ચાલુ કરો"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-hi/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-hi/strings.xml
index 003478d..a5ac0f3 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-hi/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-hi/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ड्राइवर"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"अनुमानित जगह की जानकारी सिर्फ़ तब ऐक्सेस करें, जब ऐप्लिकेशन स्क्रीन पर खुला हो"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"माइक्रोफ़ोन चालू करें"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-hr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-hr/strings.xml
index 4cffd31..0ac52d4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-hr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-hr/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Vozač"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"pristupiti približnoj lokaciji samo u prednjem planu"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Omogući mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-hu/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-hu/strings.xml
index 52b7760..778f57c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-hu/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-hu/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Sofőr"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"megközelítőleges helyadatokhoz való hozzáférés csak előtérben"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Mikrofonhoz való hozzáférés engedélyezése"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-hy/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-hy/strings.xml
index 68df140..0dc5851 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-hy/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-hy/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Վարորդ"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"տեղադրության մոտավոր տվյալների հասանելիություն միայն ֆոնային ռեժիմում"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Միացնել խոսափողը"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-in/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-in/strings.xml
index 21c21a8..52cda98 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-in/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-in/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Pengemudi"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"akses perkiraan lokasi hanya saat di latar depan"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Aktifkan Mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-is/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-is/strings.xml
index 1eb3776..b922593 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-is/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-is/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Ökumaður"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"aðgangur að áætlaðri staðsetningu aðeins í forgrunni"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Kveikja á hljóðnema"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-it/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-it/strings.xml
index c704544..10c4836 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-it/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-it/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Autista"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"Accesso alla posizione approssimativa solo in primo piano"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Attiva il microfono"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-iw/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-iw/strings.xml
index a8d3bee..657ebf2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-iw/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-iw/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"נהג/ת"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"קבלת גישה למיקום משוער בחזית בלבד"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"הפעלת המיקרופון"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ja/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ja/strings.xml
index eb41c01..6da0c2c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ja/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ja/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ドライバー"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"フォアグラウンドでのみおおよその位置情報を取得"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"マイクを有効にする"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ka/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ka/strings.xml
index 4d26423..7fe766c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ka/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ka/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"მძღოლი"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"მიახლოებით მდებარეობაზე წვდომა მხოლოდ წინა პლანზე"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"მიკროფონის ჩართვა"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-kk/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-kk/strings.xml
index b12a6d5..36195f0 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-kk/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-kk/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Көлік жүргізуші"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"болжалды орналасқан жер туралы ақпаратқа тек ашық экранда кіру"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Микрофонды қосу"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-km/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-km/strings.xml
index 4975540..1a74261 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-km/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-km/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"អ្នក​បើកបរ"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ចូលប្រើ​ទីតាំង​ប្រហាក់ប្រហែល​តែនៅផ្ទៃ​ខាងមុខប៉ុណ្ណោះ"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"បើក​មីក្រូហ្វូន"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-kn/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-kn/strings.xml
index 27e9a9e..ecfa406 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-kn/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-kn/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ಡ್ರೈವರ್"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ಮುನ್ನೆಲೆಯಲ್ಲಿ ಮಾತ್ರ ಅಂದಾಜು ಸ್ಥಳವನ್ನು ಪ್ರವೇಶಿಸಿ"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"ಮೈಕ್ರೋಫೋನ್ ಸಕ್ರಿಯಗೊಳಿಸಿ"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ko/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ko/strings.xml
index 20facb1..52c715d 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ko/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ko/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"운전자"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"포그라운드에서만 대략적인 위치에 액세스"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"마이크 사용"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ky/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ky/strings.xml
index 1a43c49..e062223 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ky/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ky/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Айдоочу"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"болжолдуу аныкталган жайгашкан жерге активдүү режимде гана кирүүгө уруксат берүү"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Микрофонду иштетүү"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-lo/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-lo/strings.xml
index 877a259..fabaa3d 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-lo/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-lo/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ຄົນຂັບລົດ"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ເຂົ້າເຖິງສະຖານທີ່ໂດຍປະມານເມື່ອຢູ່ໃນພື້ນໜ້າເທົ່ານັ້ນ"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"ເປີດການນຳໃຊ້ໄມໂຄຣໂຟນ"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-lt/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-lt/strings.xml
index 134230f..a02befb 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-lt/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-lt/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Vairuotojas"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"pasiekti apytikslę vietovę, tik kai programa veikia priekiniame plane"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Įgalinti mikrofoną"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-lv/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-lv/strings.xml
index 3cc5c88..333add2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-lv/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-lv/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Vadītājs"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"piekļuve aptuvenai atrašanās vietai, tikai darbojoties priekšplānā"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Iespējot mikrofonu"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-mk/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-mk/strings.xml
index 3b1eda7..ed49dfe 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-mk/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-mk/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Возач"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"пристап до приближната локација само во преден план"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Овозможи го микрофонот"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ml/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ml/strings.xml
index 7205182..a69ea5d 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ml/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ml/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ഡ്രൈവർ"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ഏകദേശ ലൊക്കേഷൻ ഫോർഗ്രൗണ്ടിൽ മാത്രം ആക്‌സസ് ചെയ്യുക"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"മൈക്രോഫോൺ പ്രവർത്തനക്ഷമമാക്കുക"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-mn/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-mn/strings.xml
index dd1d6a2..5b3d6fb 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-mn/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-mn/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Жолооч"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ойролцоо байршилд зөвхөн дэлгэц дээр хандах"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Микрофоныг идэвхжүүлэх"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-mr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-mr/strings.xml
index a0619ec..30efa52 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-mr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-mr/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ड्रायव्हर"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"फक्त फोअरग्राउंडमध्ये अंदाजे स्थान अ‍ॅक्सेस करा"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"मायक्रोफोन सुरू करा"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ms/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ms/strings.xml
index 7f22021..6e830f7 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ms/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ms/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Pemandu"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"akses lokasi anggaran hanya di latar depan"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Dayakan Mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-my/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-my/strings.xml
index aea1568..43c3d37 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-my/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-my/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ယာဉ်မောင်းသူ"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"မျက်နှာစာတွင်သာ ခန့်မှန်းခြေ တည်နေရာ အသုံးပြုခြင်း"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"မိုက်ခရိုဖုန်း ဖွင့်ရန်"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-nb/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-nb/strings.xml
index 6e0de84..cbc2918 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-nb/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-nb/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Sjåfør"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"bare tilgang til omtrentlig posisjon i forgrunnen"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Slå på mikrofonen"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ne/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ne/strings.xml
index 76ca044..eda9747 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ne/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ne/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"चालक"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"अग्रभूमिमा मात्र अनुमानित स्थानमाथि पहुँच राख्नुहोस्"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"माइक्रोफोन अन गर्नुहोस्"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-nl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-nl/strings.xml
index 873fb01..d79cba2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-nl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-nl/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Chauffeur"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"alleen toegang tot geschatte locatie op de voorgrond"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Microfoon aanzetten"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-or/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-or/strings.xml
index 964683f..939b105 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-or/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-or/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ଡ୍ରାଇଭର୍"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"କେବଳ ଫୋର୍‌ଗ୍ରାଉଣ୍ଡରେ ହାରାହାରି ଲୋକେସନ୍ ଆକ୍ସେସ୍ କରନ୍ତୁ"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"ମାଇକ୍ରୋଫୋନକୁ ସକ୍ଷମ କରନ୍ତୁ"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-pa/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-pa/strings.xml
index 16541d3..2a2e55e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-pa/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-pa/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ਡਰਾਈਵਰ"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ਸਿਰਫ਼ ਫੋਰਗ੍ਰਾਊਂਡ ਵਿੱਚ ਅਨੁਮਾਨਿਤ ਟਿਕਾਣੇ ਤੱਕ ਪਹੁੰਚ ਕਰੋ"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਨੂੰ ਚਾਲੂ ਕਰੋ"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-pl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-pl/strings.xml
index d0ff092..32fe8a4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-pl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-pl/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Kierowca"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"dostęp do przybliżonej lokalizacji tylko na pierwszym planie"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Włącz mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-pt-rPT/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-pt-rPT/strings.xml
index 984000a..016617e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-pt-rPT/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-pt-rPT/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Condutor"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"apenas aceder à localização aproximada em primeiro plano"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Ativar microfone"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-pt/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-pt/strings.xml
index 7ac9eef..efca2bf 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-pt/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-pt/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Motorista"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"acessar local aproximado apenas em primeiro plano"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Ativar microfone"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ro/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ro/strings.xml
index ec78db2..adf01f0 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ro/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ro/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Șofer"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"să acceseze locația aproximativă numai în prim-plan"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Activați microfonul"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ru/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ru/strings.xml
index ed49d76..12a3f43 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ru/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ru/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Водитель"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"Доступ к приблизительному местоположению только в активном режиме"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Включить"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-si/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-si/strings.xml
index fcba27e..9d75b4f 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-si/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-si/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"රියදුරු"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"පෙරබිම තුළ පමණක් ආසන්න ස්ථානයට ප්‍රවේශය"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"මයික්‍රෆෝනය සබල කරන්න"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sk/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sk/strings.xml
index c9a990d..125cb5a 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sk/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sk/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Vodič"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"prístup k približnej polohe iba v popredí"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Aktivovať mikrofón"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sl/strings.xml
index 918dda4..52841ab 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sl/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Voznik"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"dostop do približne lokacije samo, ko deluje v ospredju"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Omogoči mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sq/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sq/strings.xml
index e4192e7..dc7bbe2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sq/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sq/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Drejtuesi"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"qasu në vendndodhjen e përafërt vetëm në plan të parë"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Aktivizo mikrofonin"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sr/strings.xml
index 8cce889..2cfe36e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sr/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Возач"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"приступ приближној локацији само у првом плану"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Омогући микрофон"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sv/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sv/strings.xml
index b5b4f63..1aa7c0c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sv/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sv/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Förare"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"endast åtkomst till ungefärlig plats i förgrunden"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Aktivera mikrofon"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sw/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sw/strings.xml
index 765973c..60e5469 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sw/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sw/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Dereva"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"kufikia mahali palipokadiriwa ikiwa tu programu imefunguliwa kwenye skrini"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Washa Maikrofoni"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sw600dp/config.xml b/car_product/overlay/frameworks/base/core/res/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..f7873a2
--- /dev/null
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sw600dp/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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Enable dynamic keyguard positioning for large-width screens. This will cause the keyguard
+     to be aligned to one side of the screen when in landscape mode. -->
+    <bool name="config_enableDynamicKeyguardPositioning">false</bool>
+</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ta/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ta/strings.xml
index 264f0c1..38a2e03 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ta/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ta/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"கார் உரிமையாளர்"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"முன்புலத்தில் இயங்கும்போது மட்டும் தோராயமான இருப்பிடத்தைக் கண்டறிதல்"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"மைக்ரோஃபோனை இயக்கு"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-te/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-te/strings.xml
index bf45f96..c95285e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-te/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-te/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"డ్రైవర్"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"స్క్రీన్‌పై ఉన్నప్పుడు మాత్రమే సమీప లొకేషన్‌ను యాక్సెస్ చేయండి"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"మైక్రోఫోన్‌ను ఎనేబుల్ చేయండి"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-th/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-th/strings.xml
index 8fae2ca..6f617e3 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-th/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-th/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ผู้ขับรถ"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"เข้าถึงตำแหน่งโดยประมาณเมื่ออยู่เบื้องหน้าเท่านั้น"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"เปิดใช้ไมโครโฟน"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-tl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-tl/strings.xml
index d425a87..c42b2c8 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-tl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-tl/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Driver"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"i-access lang ang tinatantyang lokasyon sa foreground"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"I-enable ang Mikropono"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-tr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-tr/strings.xml
index 244564e..0a13258 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-tr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-tr/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Sürücü"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"yalnızca ön planda yaklaşık konuma erişme"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Mikrofonu Etkinleştir"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-uk/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-uk/strings.xml
index b46d679..c15df06 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-uk/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-uk/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Водій"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"отримувати доступ до даних про приблизне місцезнаходження лише в активному режимі"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Увімкнути мікрофон"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ur/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ur/strings.xml
index 89fd655..d3cea05 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ur/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ur/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"ڈرائیور"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"صرف پیش منظر میں تخمینی مقام تک رسائی"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"مائیکروفون فعال کریں"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-uz/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-uz/strings.xml
index 9914c10..d15ff7c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-uz/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-uz/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Haydovchi"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"taxminiy joylashuv axborotini olishga faqat old fonda ruxsat"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Mikrofonni yoqish"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-vi/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-vi/strings.xml
index d478052..1e19523 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-vi/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-vi/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Tài xế"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"chỉ truy cập thông tin vị trí gần đúng khi ứng dụng mở trên màn hình"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Cấp quyền truy cập micrô"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-zh-rCN/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-zh-rCN/strings.xml
index 06fe50d..6556c7b 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-zh-rCN/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-zh-rCN/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"司机"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"只有在前台运行时才能获取大致位置信息"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"启用麦克风"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-zh-rHK/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-zh-rHK/strings.xml
index fd93b30..0d8543b 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-zh-rHK/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-zh-rHK/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"司機"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"只在前景存取概略位置"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"啟用麥克風"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-zh-rTW/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-zh-rTW/strings.xml
index 59f09e1..7340ecd 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-zh-rTW/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-zh-rTW/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"駕駛"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"僅可在前景中取得概略位置"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"啟用麥克風"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-zu/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-zu/strings.xml
index adb9402..c99c505 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-zu/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-zu/strings.xml
@@ -19,4 +19,5 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="owner_name" msgid="3416113395996003764">"Umshayeli"</string>
     <string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"finyelela indawo enembile kuphela engaphambili"</string>
+    <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2093486820466005919">"Nika amandla Imakrofoni"</string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index d7c566c..73326f1 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -151,4 +151,15 @@
     <!-- Whether this device is supporting the microphone toggle -->
     <bool name="config_supportsMicToggle">true</bool>
 
+    <!-- Whether the airplane mode should be reset when device boots in non-safemode after exiting
+     from safemode.
+     This flag should be enabled only when the product does not have any UI to toggle airplane
+     mode like automotive devices.-->
+    <bool name="config_autoResetAirplaneMode">true</bool>
+
+    <!-- The component name of the activity for the companion-device-manager notification access
+         confirmation. -->
+    <string name="config_notificationAccessConfirmationActivity" translatable="false">
+        com.android.car.settings/com.android.car.settings.notifications.NotificationAccessConfirmationActivity
+    </string>
 </resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/policy_exempt_apps.xml b/car_product/overlay/frameworks/base/core/res/res/values/policy_exempt_apps.xml
index 9ab0ed3..27986a4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/policy_exempt_apps.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/policy_exempt_apps.xml
@@ -20,7 +20,6 @@
     device policies or APIs.
     -->
     <string-array translatable="false" name="policy_exempt_apps">
-        <item>com.android.car.carlauncher</item>
         <item>com.android.car.cluster.home</item>
         <item>com.android.car.hvac</item>
         <item>com.android.car.media</item>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/required_apps_managed_user.xml b/car_product/overlay/frameworks/base/core/res/res/values/required_apps_managed_user.xml
new file mode 100644
index 0000000..a04429e
--- /dev/null
+++ b/car_product/overlay/frameworks/base/core/res/res/values/required_apps_managed_user.xml
@@ -0,0 +1,43 @@
+<?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-array translatable="false" name="required_apps_managed_user">
+
+        <!-- NOTE: apps below were copied from phone, replacing the equivalent
+             car app when needed -->
+        <item>com.android.car.settings</item>
+        <item>com.android.systemui</item>
+        <item>com.android.car.dialer</item>
+        <item>com.android.contacts</item>
+        <item>com.android.stk</item>
+        <item>com.android.providers.downloads</item>
+        <item>com.android.providers.downloads.ui</item>
+        <item>com.android.documentsui</item>
+
+        <!-- Car-specific apps -->
+        <item>com.android.car.bugreport</item>
+        <item>com.android.car.acast.source</item>
+        <item>com.android.car.calendar</item>
+        <item>com.android.car.dialer</item>
+        <item>com.android.car.messenger</item>
+        <item>com.android.car.radio</item>
+        <item>com.android.car.speedbump</item>
+        <item>com.android.car.themeplayground</item>
+        <item>com.android.car.voicecontrol</item>
+
+    </string-array>
+</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
index 34fd92f..eff76d4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
@@ -19,4 +19,6 @@
     <!-- Default name of the owner user [CHAR LIMIT=20] -->
     <string name="owner_name">Driver</string>
     <string name="permlab_accessCoarseLocation">access approximate location only in the foreground</string>
+    <!--- Action button in the dialog triggered if microphone is disabled but an app tried to access it. [CHAR LIMIT=60] -->
+    <string name="sensor_privacy_start_use_dialog_turn_on_button">Enable Microphone</string>
 </resources>
diff --git a/car_product/rro/CarSystemUIEvsRRO/Android.bp b/car_product/rro/CarSystemUIEvsRRO/Android.bp
deleted file mode 100644
index ebb09ef..0000000
--- a/car_product/rro/CarSystemUIEvsRRO/Android.bp
+++ /dev/null
@@ -1,25 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-android_app {
-    name: "CarSystemUIEvsRRO",
-    resource_dirs: ["res"],
-    platform_apis: true,
-    aaptflags: [
-        "--no-resource-deduping",
-        "--no-resource-removal"
-    ],
-}
diff --git a/car_product/rro/CarSystemUIEvsRRO/AndroidManifest.xml b/car_product/rro/CarSystemUIEvsRRO/AndroidManifest.xml
deleted file mode 100644
index f1dc7b8..0000000
--- a/car_product/rro/CarSystemUIEvsRRO/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +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.
-  -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.systemui.car.evs.rro">
-    <application android:hasCode="false"/>
-    <overlay android:targetName="CarSystemUI"
-             android:targetPackage="com.android.systemui"
-             android:resourcesMap="@xml/overlays"
-             android:isStatic="true" />
-</manifest>
diff --git a/car_product/rro/CarSystemUIEvsRRO/res/values/config.xml b/car_product/rro/CarSystemUIEvsRRO/res/values/config.xml
deleted file mode 100644
index 4fab05c..0000000
--- a/car_product/rro/CarSystemUIEvsRRO/res/values/config.xml
+++ /dev/null
@@ -1,21 +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.
-  -->
-<resources>
-    <string name="config_rearViewCameraActivity" translatable="false">
-        com.google.android.car.evs/com.google.android.car.evs.CarEvsCameraPreviewActivity
-    </string>
-</resources>
diff --git a/car_product/rro/CarSystemUIEvsRRO/res/xml/overlays.xml b/car_product/rro/CarSystemUIEvsRRO/res/xml/overlays.xml
deleted file mode 100644
index 36785dc..0000000
--- a/car_product/rro/CarSystemUIEvsRRO/res/xml/overlays.xml
+++ /dev/null
@@ -1,20 +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.
-  -->
-
-<overlay>
-    <item target="string/config_rearViewCameraActivity"
-          value="@string/config_rearViewCameraActivity"/>
-</overlay>
diff --git a/car_product/sepolicy/private/carservice_app.te b/car_product/sepolicy/private/carservice_app.te
index 04a5808..87fd0b7 100644
--- a/car_product/sepolicy/private/carservice_app.te
+++ b/car_product/sepolicy/private/carservice_app.te
@@ -103,3 +103,6 @@
 allow carservice_app gpu_device:dir r_dir_perms;
 allow carservice_app gpu_service:service_manager find;
 binder_call(carservice_app, gpuservice)
+
+# Allow reading and writing /proc/loadavg/
+allow carservice_app proc_loadavg:file { open read getattr };
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/evs/manager/sepolicy/private/evs_manager.te b/cpp/evs/manager/sepolicy/private/evs_manager.te
index d8dba4b..ae7ec68 100644
--- a/cpp/evs/manager/sepolicy/private/evs_manager.te
+++ b/cpp/evs/manager/sepolicy/private/evs_manager.te
@@ -15,3 +15,6 @@
 
 # allow to use carservice_app
 binder_call(evs_manager, carservice_app)
+
+# allow to use the graphics allocator
+allow evs_manager hal_graphics_allocator:fd use;
diff --git a/cpp/evs/sampleDriver/sepolicy/private/evs_driver.te b/cpp/evs/sampleDriver/sepolicy/private/evs_driver.te
index 5b847fa..ab0c251 100644
--- a/cpp/evs/sampleDriver/sepolicy/private/evs_driver.te
+++ b/cpp/evs/sampleDriver/sepolicy/private/evs_driver.te
@@ -14,7 +14,11 @@
 hal_client_domain(hal_evs_driver, hal_graphics_composer)
 hal_client_domain(hal_evs_driver, hal_configstore)
 
+# Allow the driver to access EGL
 allow hal_evs_driver gpu_device:chr_file rw_file_perms;
+allow hal_evs_driver gpu_device:dir search;
+
+# Allow the driver to use SurfaceFlinger
 binder_call(hal_evs_driver, surfaceflinger);
 allow hal_evs_driver surfaceflinger_service:service_manager find;
 allow hal_evs_driver ion_device:chr_file r_file_perms;
@@ -25,4 +29,4 @@
 # Allow the driver to use automotive display proxy service
 allow hal_evs_driver automotive_display_service_server:binder call;
 allow hal_evs_driver fwk_automotive_display_hwservice:hwservice_manager find;
-
+allow hal_evs_driver automotive_display_service:fd use;
diff --git a/cpp/evs/sampleDriver/sepolicy/private/surfaceflinger.te b/cpp/evs/sampleDriver/sepolicy/private/surfaceflinger.te
index ce51a0d..d7aba85 100644
--- a/cpp/evs/sampleDriver/sepolicy/private/surfaceflinger.te
+++ b/cpp/evs/sampleDriver/sepolicy/private/surfaceflinger.te
@@ -1,2 +1,5 @@
 # Allow surfaceflinger to perform binder IPC to hal_evs_driver
 binder_call(surfaceflinger, hal_evs_driver)
+
+# Allow surfaceflinger to perform binder IPC to automotive_display_service
+binder_call(surfaceflinger, automotive_display_service)
diff --git a/cpp/security/vehicle_binding_util/Android.bp b/cpp/security/vehicle_binding_util/Android.bp
new file mode 100644
index 0000000..bea8afa
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/Android.bp
@@ -0,0 +1,67 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_defaults {
+    name: "vehicle_binding_util_defaults",
+    cflags: [
+        "-Wall",
+        "-Wno-missing-field-initializers",
+        "-Werror",
+        "-Wno-unused-variable",
+    ],
+    shared_libs: [
+        "android.hardware.automotive.vehicle@2.0",
+        "libbase",
+        "libbinder",
+        "libcutils",
+        "libhidlbase",
+        "liblog",
+        "liblogwrap",
+        "libutils",
+    ],
+    static_libs: [
+        "libbase",
+    ],
+}
+
+cc_library_static {
+    name: "libvehicle_binding_util",
+    srcs: [
+        "src/VehicleBindingUtil.cpp",
+    ],
+    defaults: [
+        "vehicle_binding_util_defaults",
+    ],
+    export_include_dirs: [
+        "src",
+    ],
+}
+
+cc_binary {
+    name: "vehicle_binding_util",
+    defaults: [
+        "vehicle_binding_util_defaults",
+    ],
+    srcs: [
+        "src/main.cpp",
+    ],
+    init_rc: ["vehicle_binding_util.rc"],
+    static_libs: [
+        "libvehicle_binding_util",
+    ],
+}
diff --git a/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp
new file mode 100644
index 0000000..c15bd3c
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp
@@ -0,0 +1,197 @@
+/*
+ * 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.
+ */
+
+#include "VehicleBindingUtil.h"
+
+#include <android-base/logging.h>
+#include <android/hardware/automotive/vehicle/2.0/types.h>
+#include <cutils/properties.h>  // for property_get
+#include <logwrap/logwrap.h>
+#include <utils/SystemClock.h>
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/random.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace security {
+namespace {
+
+using android::hardware::automotive::vehicle::V2_0::IVehicle;
+using android::hardware::automotive::vehicle::V2_0::StatusCode;
+using android::hardware::automotive::vehicle::V2_0::VehicleArea;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropConfig;
+using android::hardware::automotive::vehicle::V2_0::VehicleProperty;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropertyStatus;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropValue;
+
+template <typename T>
+using hidl_vec = android::hardware::hidl_vec<T>;
+
+bool isSeedVhalPropertySupported(sp<IVehicle> vehicle) {
+    bool is_supported = false;
+
+    hidl_vec<int32_t> props = {
+            static_cast<int32_t>(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED)};
+    vehicle->getPropConfigs(props,
+                            [&is_supported](StatusCode status,
+                                            hidl_vec<VehiclePropConfig> /*propConfigs*/) {
+                                is_supported = (status == StatusCode::OK);
+                            });
+    return is_supported;
+}
+
+std::string toHexString(const std::vector<uint8_t>& bytes) {
+    const char lookup[] = "0123456789abcdef";
+    std::string out;
+    out.reserve(bytes.size() * 2);
+    for (auto b : bytes) {
+        out += lookup[b >> 4];
+        out += lookup[b & 0xf];
+    }
+    return out;
+}
+
+BindingStatus setSeedVhalProperty(sp<IVehicle> vehicle, const std::vector<uint8_t>& seed) {
+    VehiclePropValue propValue;
+    propValue.timestamp = elapsedRealtimeNano();
+    propValue.areaId = toInt(VehicleArea::GLOBAL);
+    propValue.prop = toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED);
+    propValue.status = VehiclePropertyStatus::AVAILABLE;
+    propValue.value.bytes = seed;
+    StatusCode vhal_status = vehicle->set(propValue);
+    if (vhal_status == StatusCode::OK) {
+        return BindingStatus::OK;
+    }
+
+    LOG(ERROR) << "Unable to set the VHAL property: " << toString(vhal_status);
+    return BindingStatus::ERROR;
+}
+
+BindingStatus getSeedVhalProperty(sp<IVehicle> vehicle, std::vector<uint8_t>* seed) {
+    VehiclePropValue desired_prop;
+    desired_prop.prop = static_cast<int32_t>(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED);
+    BindingStatus status = BindingStatus::ERROR;
+    vehicle->get(desired_prop,
+                 [&status, &seed](StatusCode prop_status, const VehiclePropValue& propValue) {
+                     if (prop_status != StatusCode::OK) {
+                         LOG(ERROR) << "Error reading vehicle property: " << toString(prop_status);
+                     } else {
+                         status = BindingStatus::OK;
+                         *seed = std::vector<uint8_t>{propValue.value.bytes.begin(),
+                                                      propValue.value.bytes.end()};
+                     }
+                 });
+
+    return status;
+}
+
+BindingStatus sendSeedToVold(const Executor& executor, const std::vector<uint8_t>& seed) {
+    int status = 0;
+
+    // we pass the seed value via environment variable in the forked process
+    setenv("SEED_VALUE", toHexString(seed).c_str(), 1);
+    int rc = executor.run({"/system/bin/vdc", "cryptfs", "bindkeys"}, &status);
+    unsetenv("SEED_VALUE");
+    LOG(INFO) << "rc: " << rc;
+    LOG(INFO) << "status: " << status;
+    if (rc != 0 || status != 0) {
+        LOG(ERROR) << "Error running vdc: " << rc << ", " << status;
+        return BindingStatus::ERROR;
+    }
+    return BindingStatus::OK;
+}
+
+}  // namespace
+
+bool DefaultCsrng::fill(void* buffer, size_t size) const {
+    int fd = TEMP_FAILURE_RETRY(open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOFOLLOW));
+    if (fd == -1) {
+        LOG(ERROR) << "Error opening urandom: " << errno;
+        return false;
+    }
+
+    ssize_t bytes_read;
+    uint8_t* bufptr = static_cast<uint8_t*>(buffer);
+    while ((bytes_read = TEMP_FAILURE_RETRY(read(fd, bufptr, size))) > 0) {
+        size -= bytes_read;
+        bufptr += bytes_read;
+    }
+
+    close(fd);
+
+    if (size != 0) {
+        LOG(ERROR) << "Unable to read " << size << " bytes from urandom";
+        return false;
+    }
+    return true;
+}
+
+int DefaultExecutor::run(const std::vector<std::string>& cmd_args, int* exit_code) const {
+    std::vector<const char*> argv;
+    argv.reserve(cmd_args.size());
+    for (auto& arg : cmd_args) {
+        argv.push_back(arg.c_str());
+    }
+    int status = 0;
+    return logwrap_fork_execvp(argv.size(), argv.data(), exit_code, false /*forward_signals*/,
+                               LOG_KLOG, true /*abbreviated*/, nullptr /*file_path*/);
+}
+
+BindingStatus setVehicleBindingSeed(sp<IVehicle> vehicle, const Executor& executor,
+                                    const Csrng& csrng) {
+    if (!isSeedVhalPropertySupported(vehicle)) {
+        LOG(WARNING) << "Vehicle binding seed is not supported by the VHAL.";
+        return BindingStatus::NOT_SUPPORTED;
+    }
+
+    std::vector<uint8_t> seed;
+    BindingStatus status = getSeedVhalProperty(vehicle, &seed);
+    if (status != BindingStatus::OK) {
+        LOG(ERROR) << "Unable to read the seed from the VHAL: " << static_cast<int>(status);
+        return status;
+    }
+
+    if (seed.empty()) {
+        seed = std::vector<uint8_t>(SEED_BYTE_SIZE);
+        if (!csrng.fill(seed.data(), seed.size())) {
+            LOG(ERROR) << "Error getting random seed: " << static_cast<int>(status);
+            return BindingStatus::ERROR;
+        }
+
+        status = setSeedVhalProperty(vehicle, seed);
+        if (status != BindingStatus::OK) {
+            LOG(ERROR) << "Error storing the seed in the VHAL: " << static_cast<int>(status);
+            return status;
+        }
+    }
+
+    status = sendSeedToVold(executor, seed);
+    if (status == BindingStatus::OK) {
+        LOG(INFO) << "Successfully bound vehicle storage to seed.";
+    }
+    return status;
+}
+
+}  // namespace security
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.h b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.h
new file mode 100644
index 0000000..3fa89ec
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.h
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+#ifndef CPP_SECURITY_VEHICLE_BINDING_UTIL_SRC_VEHICLEBINDINGUTIL_H_
+#define CPP_SECURITY_VEHICLE_BINDING_UTIL_SRC_VEHICLEBINDINGUTIL_H_
+
+#include "android/hardware/automotive/vehicle/2.0/types.h"
+
+#include <android/hardware/automotive/vehicle/2.0/IVehicle.h>
+#include <utils/StrongPointer.h>
+
+#include <cstdint>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace security {
+
+constexpr size_t SEED_BYTE_SIZE = 16;
+
+// Possible results of attempting to set the vehicle binding seed.
+enum class BindingStatus {
+    OK,
+    NOT_SUPPORTED,
+    ERROR,
+};
+
+template <typename EnumT>
+constexpr auto toInt(const EnumT value) {
+    return static_cast<typename std::underlying_type<EnumT>::type>(value);
+}
+
+// Interface for getting cryptographically secure random byte strings
+class Csrng {
+public:
+    virtual ~Csrng() = default;
+
+    // Fill the given buffer with random bytes. Returns false if there is
+    // an unrecoverable error getting bits.
+    virtual bool fill(void* buffer, size_t size) const = 0;
+};
+
+// Csrng that relies on `/dev/urandom` to supply bits. We have to rely on
+// urandom so that we don't block boot-up. Devices that wish to supply very
+// high-quality random bits at boot should seed the linux PRNG at boot with
+// entropy.
+class DefaultCsrng : public Csrng {
+public:
+    bool fill(void* buffer, size_t size) const override;
+};
+
+// Interface for forking and executing a child process.
+class Executor {
+public:
+    virtual ~Executor() = default;
+
+    // Run the given command line and its arguments. Returns 0 on success, -1
+    // if an internal error occurred, and -ECHILD if the child process did not
+    // exit properly.
+    //
+    // On exit, `exit_code` is set to the child's exit status.
+    virtual int run(const std::vector<std::string>& cmd_args, int* exit_code) const = 0;
+};
+
+// Default Executor which forks, execs, and logs output from the child process.
+class DefaultExecutor : public Executor {
+    int run(const std::vector<std::string>& cmd_args, int* exit_code) const override;
+};
+
+// Set the seed in vold that is used to bind the encryption keys to the vehicle.
+// This is used to guard against headunit removal and subsequent scraping of
+// the filesystem for sensitive data (e.g. PII).
+//
+// The seed is read from the VHAL property STORAGE_ENCRYPTION_BINDING_SEED. If
+// the property has not yet been set, a random byte value is generated and
+// saved in the VHAL for reuse on future boots.
+BindingStatus setVehicleBindingSeed(
+        sp<::android::hardware::automotive::vehicle::V2_0::IVehicle> vehicle,
+        const Executor& executor, const Csrng& csrng);
+
+}  // namespace security
+}  // namespace automotive
+}  // namespace android
+
+#endif  // CPP_SECURITY_VEHICLE_BINDING_UTIL_SRC_VEHICLEBINDINGUTIL_H_
diff --git a/cpp/security/vehicle_binding_util/src/main.cpp b/cpp/security/vehicle_binding_util/src/main.cpp
new file mode 100644
index 0000000..cd82017
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/src/main.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+#include "VehicleBindingUtil.h"
+
+#include <android-base/logging.h>
+#include <android/hardware/automotive/vehicle/2.0/IVehicle.h>
+#include <binder/IServiceManager.h>
+#include <binder/Status.h>
+
+#include <iostream>
+#include <map>
+#include <string>
+
+namespace {
+
+using android::defaultServiceManager;
+using android::automotive::security::BindingStatus;
+using android::automotive::security::DefaultCsrng;
+using android::automotive::security::DefaultExecutor;
+using android::hardware::automotive::vehicle::V2_0::IVehicle;
+
+static int printHelp(int argc, char* argv[]);
+static int setBinding(int /*argc*/, char*[] /*argv*/);
+
+// Avoid calling complex destructor on cleanup.
+const auto& subcommandTable = *new std::map<std::string, std::function<int(int, char*[])>>{
+        {"help", printHelp},
+        {"set_binding", setBinding},
+};
+
+static int setBinding(int /*argc*/, char*[] /*argv*/) {
+    auto status = setVehicleBindingSeed(IVehicle::getService(), DefaultExecutor{}, DefaultCsrng{});
+    if (status != BindingStatus::OK) {
+        LOG(ERROR) << "Unable to set the binding seed. Encryption keys are not "
+                   << "bound to the platform.";
+        return static_cast<int>(status);
+    }
+
+    return 0;
+}
+
+static int printHelp(int /*argc*/, char* argv[]) {
+    std::cout << "Usage: " << argv[0] << " <subcommand> [args]" << std::endl
+              << "Valid subcommands: " << std::endl;
+    for (const auto& i : subcommandTable) {
+        std::cout << "    " << i.first << std::endl;
+    }
+    return 0;
+}
+
+}  // namespace
+
+int main(int argc, char* argv[]) {
+    setenv("ANDROID_LOG_TAGS", "*:v", 1);
+    android::base::InitLogging(argv,
+                               (getppid() == 1) ? &android::base::KernelLogger
+                                                : &android::base::StderrLogger);
+    if (argc < 2) {
+        LOG(ERROR) << "Please specify a subcommand.";
+        printHelp(argc, argv);
+        return -1;
+    }
+
+    auto subcommand = subcommandTable.find(argv[1]);
+    if (subcommand == subcommandTable.end()) {
+        LOG(ERROR) << "Invalid subcommand: " << argv[1];
+        printHelp(argc, argv);
+        return -1;
+    }
+
+    return subcommand->second(argc, argv);
+}
diff --git a/cpp/security/vehicle_binding_util/tests/Android.bp b/cpp/security/vehicle_binding_util/tests/Android.bp
new file mode 100644
index 0000000..d47b8e5
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/tests/Android.bp
@@ -0,0 +1,68 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+    name: "libvehicle_binding_util_test",
+    defaults: [
+        "vehicle_binding_util_defaults",
+    ],
+    test_suites: ["general-tests"],
+    srcs: [
+        "VehicleBindingUtilTests.cpp",
+    ],
+    static_libs: [
+        "libbase",
+        "libgmock",
+        "libgtest",
+        "libvehicle_binding_util",
+    ],
+}
+
+cc_test {
+    name: "vehicle_binding_integration_test",
+    test_suites: [
+        "device-tests",
+        "vts"
+    ],
+    require_root: true,
+    defaults: ["vehicle_binding_util_defaults"],
+    tidy: false,
+    srcs: [
+        "VehicleBindingIntegrationTedt.cpp"
+    ],
+    shared_libs: [
+        "android.hardware.automotive.vehicle@2.0",
+        "libbase",
+        "libbinder",
+        "libhidlbase",
+        "libutils",
+    ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+    sanitize: {
+        address: false,
+        recover: [ "all" ],
+    },
+}
diff --git a/cpp/security/vehicle_binding_util/tests/VehicleBindingIntegrationTedt.cpp b/cpp/security/vehicle_binding_util/tests/VehicleBindingIntegrationTedt.cpp
new file mode 100644
index 0000000..50f358e
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/tests/VehicleBindingIntegrationTedt.cpp
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+#include <android-base/properties.h>
+#include <android/hardware/automotive/vehicle/2.0/IVehicle.h>
+#include <gtest/gtest.h>
+
+namespace android {
+namespace automotive {
+namespace security {
+namespace {
+
+using android::hardware::automotive::vehicle::V2_0::IVehicle;
+using android::hardware::automotive::vehicle::V2_0::StatusCode;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropConfig;
+using android::hardware::automotive::vehicle::V2_0::VehicleProperty;
+
+template <typename T>
+using hidl_vec = android::hardware::hidl_vec<T>;
+
+bool isSeedVhalPropertySupported(sp<IVehicle> vehicle) {
+    bool is_supported = false;
+
+    hidl_vec<int32_t> props = {
+            static_cast<int32_t>(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED)};
+    vehicle->getPropConfigs(props,
+                            [&is_supported](StatusCode status,
+                                            hidl_vec<VehiclePropConfig> /*propConfigs*/) {
+                                is_supported = (status == StatusCode::OK);
+                            });
+    return is_supported;
+}
+
+// Verify that vold got the binding seed if VHAL reports a seed
+TEST(VehicleBindingIntegrationTedt, TestVehicleBindingSeedSet) {
+    std::string expected_value = "1";
+    if (!isSeedVhalPropertySupported(IVehicle::getService())) {
+        GTEST_LOG_(INFO) << "Device does not support vehicle binding seed "
+                            "(STORAGE_ENCRYPTION_BINDING_SEED).";
+        expected_value = "";
+    }
+
+    ASSERT_EQ(expected_value, android::base::GetProperty("vold.storage_seed_bound", ""));
+}
+
+}  // namespace
+}  // namespace security
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/security/vehicle_binding_util/tests/VehicleBindingUtilTests.cpp b/cpp/security/vehicle_binding_util/tests/VehicleBindingUtilTests.cpp
new file mode 100644
index 0000000..dc3ed6e
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/tests/VehicleBindingUtilTests.cpp
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+
+#include "VehicleBindingUtil.h"
+
+#include <android/hardware/automotive/vehicle/2.0/IVehicle.h>
+#include <android/hardware/automotive/vehicle/2.0/types.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <hidl/Status.h>
+#include <utils/SystemClock.h>
+
+#include <iterator>
+
+namespace android {
+namespace automotive {
+namespace security {
+namespace {
+
+using android::hardware::Void;
+using android::hardware::automotive::vehicle::V2_0::IVehicle;
+using android::hardware::automotive::vehicle::V2_0::IVehicleCallback;
+using android::hardware::automotive::vehicle::V2_0::StatusCode;
+using android::hardware::automotive::vehicle::V2_0::SubscribeOptions;
+using android::hardware::automotive::vehicle::V2_0::VehicleProperty;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropValue;
+
+template <typename T>
+using hidl_vec = android::hardware::hidl_vec<T>;
+template <typename T>
+using VhalReturn = android::hardware::Return<T>;
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::ElementsAreArray;
+using ::testing::NotNull;
+using ::testing::Return;
+using ::testing::SetArgPointee;
+using ::testing::Test;
+
+class MockVehicle : public IVehicle {
+public:
+    MOCK_METHOD(VhalReturn<void>, getAllPropConfigs, (getAllPropConfigs_cb), (override));
+
+    MOCK_METHOD(VhalReturn<void>, getPropConfigs, (const hidl_vec<int32_t>&, getPropConfigs_cb),
+                (override));
+
+    MOCK_METHOD(VhalReturn<void>, get, (const VehiclePropValue&, get_cb), (override));
+
+    MOCK_METHOD(VhalReturn<StatusCode>, set, (const VehiclePropValue&), (override));
+
+    MOCK_METHOD(VhalReturn<StatusCode>, subscribe,
+                (const sp<IVehicleCallback>&, const hidl_vec<SubscribeOptions>&), (override));
+
+    MOCK_METHOD(VhalReturn<StatusCode>, unsubscribe, (const sp<IVehicleCallback>&, int32_t),
+                (override));
+
+    MOCK_METHOD(VhalReturn<void>, debugDump, (debugDump_cb), (override));
+};
+
+class MockCsrng : public Csrng {
+public:
+    MOCK_METHOD(bool, fill, (void*, size_t), (const override));
+};
+
+class MockExecutor : public Executor {
+public:
+    MOCK_METHOD(int, run, (const std::vector<std::string>&, int*), (const override));
+};
+
+class VehicleBindingUtilTests : public Test {
+protected:
+    void setMockVhalPropertySupported() {
+        hidl_vec<int32_t> expectedProps = {toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED)};
+        EXPECT_CALL(*mMockVehicle, getPropConfigs(expectedProps, _))
+                .WillOnce([](const hidl_vec<int32_t>&, IVehicle::getPropConfigs_cb callback) {
+                    callback(StatusCode::OK, {});
+                    return Void();
+                });
+    }
+
+    void setMockVhalPropertyValue(const std::vector<uint8_t>& seed) {
+        EXPECT_CALL(*mMockVehicle, get(_, _))
+                .WillOnce([seed](const VehiclePropValue& propValue, IVehicle::get_cb callback) {
+                    EXPECT_EQ(propValue.prop,
+                              toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED));
+                    VehiclePropValue value;
+                    value.prop = propValue.prop;
+                    value.value.bytes = hidl_vec<uint8_t>{seed.begin(), seed.end()};
+                    callback(StatusCode::OK, value);
+                    return Void();
+                });
+    }
+
+    void setTestRandomness(const char seed[SEED_BYTE_SIZE]) {
+        EXPECT_CALL(mMockCsrng, fill(NotNull(), SEED_BYTE_SIZE))
+                .WillOnce([seed](void* buf, size_t) {
+                    memcpy(buf, seed, SEED_BYTE_SIZE);
+                    return true;
+                });
+    }
+
+    static std::vector<uint8_t> toVector(const char seed[SEED_BYTE_SIZE]) {
+        return {seed, seed + SEED_BYTE_SIZE};
+    }
+
+    static std::vector<std::string> makeVdcArgs() {
+        return {"/system/bin/vdc", "cryptfs", "bindkeys"};
+    }
+
+    sp<MockVehicle> mMockVehicle{new MockVehicle};
+    MockExecutor mMockExecutor;
+    MockCsrng mMockCsrng;
+};
+
+// Verify that we fail as expected if the VHAL property is not supported. This
+// is not necessarily an error, and is expected on platforms that don't
+// implement the feature.
+TEST_F(VehicleBindingUtilTests, VhalPropertyUnsupported) {
+    hidl_vec<int32_t> expectedProps = {toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED)};
+    EXPECT_CALL(*mMockVehicle, getPropConfigs(expectedProps, _))
+            .WillOnce([](const hidl_vec<int32_t>&, IVehicle::getPropConfigs_cb callback) {
+                callback(StatusCode::INVALID_ARG, {});
+                return Void();
+            });
+
+    EXPECT_EQ(BindingStatus::NOT_SUPPORTED,
+              setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+// Verify that we properly handle an attempt to generate a random seed.
+TEST_F(VehicleBindingUtilTests, GetRandomnessFails) {
+    setMockVhalPropertySupported();
+    setMockVhalPropertyValue({});
+    EXPECT_CALL(mMockCsrng, fill(_, SEED_BYTE_SIZE)).WillOnce(Return(false));
+    EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+// Verify that we properly handle an attempt to generate a random seed.
+TEST_F(VehicleBindingUtilTests, GetSeedVhalPropertyFails) {
+    setMockVhalPropertySupported();
+    EXPECT_CALL(*mMockVehicle, get(_, _))
+            .WillOnce([&](const VehiclePropValue& propValue, IVehicle::get_cb callback) {
+                EXPECT_EQ(propValue.prop, toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED));
+                callback(StatusCode::NOT_AVAILABLE, {});
+                return Void();
+            });
+    EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedVhalPropertyFails) {
+    setMockVhalPropertySupported();
+    setMockVhalPropertyValue({});
+    setTestRandomness("I am not random");
+
+    EXPECT_CALL(*mMockVehicle, set(_)).WillOnce([](const VehiclePropValue&) {
+        return StatusCode::NOT_AVAILABLE;
+    });
+
+    EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedWithNewRandomSeed) {
+    setMockVhalPropertySupported();
+    setMockVhalPropertyValue({});
+    constexpr char SEED[SEED_BYTE_SIZE] = "Seed Value Here";
+    setTestRandomness(SEED);
+
+    EXPECT_CALL(*mMockVehicle, set(_)).WillOnce([&](const VehiclePropValue& value) {
+        EXPECT_EQ(value.prop, toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED));
+        EXPECT_THAT(value.value.bytes, testing::ElementsAreArray(SEED));
+        return StatusCode::OK;
+    });
+
+    EXPECT_CALL(mMockExecutor, run(ElementsAreArray(makeVdcArgs()), _)).WillOnce(Return(0));
+
+    EXPECT_EQ(BindingStatus::OK, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedWithExistingProperty) {
+    setMockVhalPropertySupported();
+    const auto SEED = toVector("16 bytes of seed");
+    setMockVhalPropertyValue(SEED);
+    EXPECT_CALL(mMockExecutor, run(ElementsAreArray(makeVdcArgs()), _)).WillOnce(Return(0));
+    EXPECT_EQ(BindingStatus::OK, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedVdcExecFails) {
+    setMockVhalPropertySupported();
+    const auto SEED = toVector("abcdefghijklmnop");
+    setMockVhalPropertyValue(SEED);
+    EXPECT_CALL(mMockExecutor, run(ElementsAreArray(makeVdcArgs()), _)).WillOnce(Return(-1));
+    EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedVdcExitsWithNonZeroStatus) {
+    setMockVhalPropertySupported();
+    const auto SEED = toVector("1123581321345589");
+    setMockVhalPropertyValue(SEED);
+    EXPECT_CALL(mMockExecutor, run(ElementsAreArray(makeVdcArgs()), _))
+            .WillOnce(DoAll(SetArgPointee<1>(-1), Return(0)));
+    EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+}  // namespace
+}  // namespace security
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/security/vehicle_binding_util/vehicle_binding_util.rc b/cpp/security/vehicle_binding_util/vehicle_binding_util.rc
new file mode 100644
index 0000000..cf73a23
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/vehicle_binding_util.rc
@@ -0,0 +1,4 @@
+service vold_seed_binding /system/bin/vehicle_binding_util set_binding
+    oneshot
+    user root
+    group root
diff --git a/cpp/telemetry/proto/Android.bp b/cpp/telemetry/proto/Android.bp
index 862a537..53be1a4 100644
--- a/cpp/telemetry/proto/Android.bp
+++ b/cpp/telemetry/proto/Android.bp
@@ -24,3 +24,9 @@
         "evs.proto",
     ],
 }
+
+filegroup {
+    name: "cartelemetry-cardata-proto-srcs",
+    srcs: ["*.proto"],
+}
+
diff --git a/cpp/telemetry/proto/CarData.proto b/cpp/telemetry/proto/CarData.proto
index 96e8c2e..e5e9b38 100644
--- a/cpp/telemetry/proto/CarData.proto
+++ b/cpp/telemetry/proto/CarData.proto
@@ -31,13 +31,18 @@
 
 import "packages/services/Car/cpp/telemetry/proto/evs.proto";
 
+// Contains all the CarData messages to declare all the messages.
+// Unique protobuf number is used as an ID.
+// A message will be sent from writer clients to the cartelemetryd
+// wrapped in
+// frameworks/hardware/interfaces/automotive/telemetry/aidl/android/frameworks/automotive/telemetry/CarData.aidl
 message CarData {
   oneof pushed {
     EvsFirstFrameLatency evs_first_frame_latency = 1;
   }
 
-  // DO NOT USE field numbers above 100,000 in AOSP.
-  // Field numbers 100,000 - 199,999 are reserved for non-AOSP (e.g. OEMs) to
-  // use. Field numbers 200,000 and above are reserved for future use; do not
+  // DO NOT USE field numbers above 10,000 in AOSP.
+  // Field numbers 10,000 - 19,999 are reserved for non-AOSP (e.g. OEMs) to
+  // use. Field numbers 20,000 and above are reserved for future use; do not
   // use them at all.
 }
diff --git a/cpp/telemetry/script_executor/Android.bp b/cpp/telemetry/script_executor/Android.bp
deleted file mode 100644
index 6eaec62..0000000
--- a/cpp/telemetry/script_executor/Android.bp
+++ /dev/null
@@ -1,77 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-cc_defaults {
-    name: "scriptexecutor_defaults",
-    cflags: [
-        "-Wno-unused-parameter",
-    ],
-    static_libs: [
-        "libbase",
-        "liblog",
-        "liblua",
-    ],
-}
-
-cc_library {
-    name: "libscriptexecutor",
-    defaults: [
-        "scriptexecutor_defaults",
-    ],
-    srcs: [
-        "src/JniUtils.cpp",
-        "src/LuaEngine.cpp",
-        "src/ScriptExecutorListener.cpp",
-    ],
-    shared_libs: [
-        "libandroid_runtime",
-        "libnativehelper",
-    ],
-    // Allow dependents to use the header files.
-    export_include_dirs: [
-        "src",
-    ],
-}
-
-cc_library_shared {
-    name: "libscriptexecutorjniutils-test",
-    defaults: [
-        "scriptexecutor_defaults",
-    ],
-    srcs: [
-        "src/tests/JniUtilsTestHelper.cpp",
-    ],
-    shared_libs: [
-        "libnativehelper",
-        "libscriptexecutor",
-    ],
-}
-
-cc_library {
-    name: "libscriptexecutorjni",
-    defaults: [
-        "scriptexecutor_defaults",
-    ],
-    srcs: [
-        "src/ScriptExecutorJni.cpp",
-    ],
-    shared_libs: [
-        "libnativehelper",
-        "libscriptexecutor",
-    ],
-}
diff --git a/cpp/telemetry/script_executor/src/JniUtils.cpp b/cpp/telemetry/script_executor/src/JniUtils.cpp
deleted file mode 100644
index 93c1af8..0000000
--- a/cpp/telemetry/script_executor/src/JniUtils.cpp
+++ /dev/null
@@ -1,95 +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.
- */
-
-#include "JniUtils.h"
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-void PushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle) {
-    lua_newtable(luaEngine->GetLuaState());
-    // null bundle object is allowed. We will treat it as an empty table.
-    if (bundle == nullptr) {
-        return;
-    }
-
-    // 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;");
-    jobject keys = env->CallObjectMethod(bundle, getKeySetMethod);
-    jclass setClass = env->FindClass("java/util/Set");
-    jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
-    jobject keySetIteratorObject = env->CallObjectMethod(keys, iteratorMethod);
-
-    jclass iteratorClass = env->FindClass("java/util/Iterator");
-    jmethodID hasNextMethod = env->GetMethodID(iteratorClass, "hasNext", "()Z");
-    jmethodID nextMethod = env->GetMethodID(iteratorClass, "next", "()Ljava/lang/Object;");
-
-    jclass booleanClass = env->FindClass("java/lang/Boolean");
-    jclass integerClass = env->FindClass("java/lang/Integer");
-    jclass numberClass = env->FindClass("java/lang/Number");
-    jclass stringClass = env->FindClass("java/lang/String");
-    // 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;");
-
-    // Iterate over key set of the bundle one key at a time.
-    while (env->CallBooleanMethod(keySetIteratorObject, hasNextMethod)) {
-        // Read the value object that corresponds to this key.
-        jstring key = (jstring)env->CallObjectMethod(keySetIteratorObject, nextMethod);
-        jobject value = env->CallObjectMethod(bundle, getMethod, key);
-
-        // Get the value of the type, extract it accordingly from the bundle and
-        // push the extracted value and the key to the Lua table.
-        if (env->IsInstanceOf(value, booleanClass)) {
-            jmethodID boolMethod = env->GetMethodID(booleanClass, "booleanValue", "()Z");
-            bool boolValue = static_cast<bool>(env->CallBooleanMethod(value, boolMethod));
-            lua_pushboolean(luaEngine->GetLuaState(), boolValue);
-        } else if (env->IsInstanceOf(value, integerClass)) {
-            jmethodID intMethod = env->GetMethodID(integerClass, "intValue", "()I");
-            lua_pushinteger(luaEngine->GetLuaState(), env->CallIntMethod(value, intMethod));
-        } else if (env->IsInstanceOf(value, numberClass)) {
-            // Condense other numeric types using one class. Because lua supports only
-            // integer or double, and we handled integer in previous if clause.
-            jmethodID numberMethod = env->GetMethodID(numberClass, "doubleValue", "()D");
-            /* Pushes a double onto the stack */
-            lua_pushnumber(luaEngine->GetLuaState(), env->CallDoubleMethod(value, numberMethod));
-        } else if (env->IsInstanceOf(value, stringClass)) {
-            const char* rawStringValue = env->GetStringUTFChars((jstring)value, nullptr);
-            lua_pushstring(luaEngine->GetLuaState(), rawStringValue);
-            env->ReleaseStringUTFChars((jstring)value, rawStringValue);
-        } else {
-            // Other types are not implemented yet, skipping.
-            continue;
-        }
-
-        const char* rawKey = env->GetStringUTFChars(key, nullptr);
-        // table[rawKey] = value, where value is on top of the stack,
-        // and the table is the next element in the stack.
-        lua_setfield(luaEngine->GetLuaState(), /* idx= */ -2, rawKey);
-        env->ReleaseStringUTFChars(key, rawKey);
-    }
-}
-
-}  // namespace script_executor
-}  // namespace telemetry
-}  // namespace automotive
-}  // namespace android
diff --git a/cpp/telemetry/script_executor/src/JniUtils.h b/cpp/telemetry/script_executor/src/JniUtils.h
deleted file mode 100644
index c3ef677..0000000
--- a/cpp/telemetry/script_executor/src/JniUtils.h
+++ /dev/null
@@ -1,39 +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.
- */
-#ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_JNIUTILS_H_
-#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_JNIUTILS_H_
-
-#include "LuaEngine.h"
-#include "jni.h"
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-// Helper function which takes android.os.Bundle object in "bundle" argument
-// and converts it to Lua table on top of Lua stack. All key-value pairs are
-// converted to the corresponding key-value pairs of the Lua table as long as
-// the Bundle value types are supported. At this point, we support boolean,
-// integer, double and String types in Java.
-void PushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle);
-
-}  // namespace script_executor
-}  // namespace telemetry
-}  // namespace automotive
-}  // namespace android
-
-#endif  // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_JNIUTILS_H_
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.cpp b/cpp/telemetry/script_executor/src/LuaEngine.cpp
deleted file mode 100644
index 1a074f2..0000000
--- a/cpp/telemetry/script_executor/src/LuaEngine.cpp
+++ /dev/null
@@ -1,95 +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.
- */
-
-#include "LuaEngine.h"
-
-#include <utility>
-
-extern "C" {
-#include "lauxlib.h"
-#include "lualib.h"
-}
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-LuaEngine::LuaEngine() {
-    // Instantiate Lua environment
-    mLuaState = luaL_newstate();
-    luaL_openlibs(mLuaState);
-}
-
-LuaEngine::~LuaEngine() {
-    lua_close(mLuaState);
-}
-
-lua_State* LuaEngine::GetLuaState() {
-    return mLuaState;
-}
-
-void LuaEngine::ResetListener(ScriptExecutorListener* listener) {
-    mListener.reset(listener);
-}
-
-int LuaEngine::LoadScript(const char* scriptBody) {
-    // As the first step in Lua script execution we want to load
-    // the body of the script into Lua stack and have it processed by Lua
-    // to catch any errors.
-    // More on luaL_dostring: https://www.lua.org/manual/5.3/manual.html#lual_dostring
-    // If error, pushes the error object into the stack.
-    const auto status = luaL_dostring(mLuaState, scriptBody);
-    if (status) {
-        // Removes error object from the stack.
-        // Lua stack must be properly maintained due to its limited size,
-        // ~20 elements and its critical function because all interaction with
-        // Lua happens via the stack.
-        // Starting read about Lua stack: https://www.lua.org/pil/24.2.html
-        // TODO(b/192284232): add test case to trigger this.
-        lua_pop(mLuaState, 1);
-    }
-    return status;
-}
-
-bool LuaEngine::PushFunction(const char* functionName) {
-    // Interaction between native code and Lua happens via Lua stack.
-    // In such model, a caller first pushes the name of the function
-    // that needs to be called, followed by the function's input
-    // arguments, one input value pushed at a time.
-    // More info: https://www.lua.org/pil/24.2.html
-    lua_getglobal(mLuaState, functionName);
-    const auto status = lua_isfunction(mLuaState, /*idx= */ -1);
-    // TODO(b/192284785): add test case for wrong function name in Lua.
-    if (status == 0) lua_pop(mLuaState, 1);
-    return status;
-}
-
-int LuaEngine::Run() {
-    // Performs blocking call of the provided Lua function. Assumes all
-    // input arguments are in the Lua stack as well in proper order.
-    // On how to call Lua functions: https://www.lua.org/pil/25.2.html
-    // Doc on lua_pcall: https://www.lua.org/manual/5.3/manual.html#lua_pcall
-    // TODO(b/189241508): Once we implement publishedData parsing, nargs should
-    // change from 1 to 2.
-    // TODO(b/192284612): add test case for failed call.
-    return lua_pcall(mLuaState, /* nargs= */ 1, /* nresults= */ 0, /*errfunc= */ 0);
-}
-
-}  // namespace script_executor
-}  // namespace telemetry
-}  // namespace automotive
-}  // namespace android
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.h b/cpp/telemetry/script_executor/src/LuaEngine.h
deleted file mode 100644
index a1d6e48..0000000
--- a/cpp/telemetry/script_executor/src/LuaEngine.h
+++ /dev/null
@@ -1,72 +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.
- */
-
-#ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_LUAENGINE_H_
-#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_LUAENGINE_H_
-
-#include "ScriptExecutorListener.h"
-
-#include <memory>
-
-extern "C" {
-#include "lua.h"
-}
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-// Encapsulates Lua script execution environment.
-class LuaEngine {
-public:
-    LuaEngine();
-
-    virtual ~LuaEngine();
-
-    // Returns pointer to Lua state object.
-    lua_State* GetLuaState();
-
-    // Loads Lua script provided as scriptBody string.
-    // Returns 0 if successful. Otherwise returns non-zero Lua error code.
-    int LoadScript(const char* scriptBody);
-
-    // Pushes a Lua function under provided name into the stack.
-    // Returns true if successful.
-    bool PushFunction(const char* functionName);
-
-    // Invokes function with the inputs provided in the stack.
-    // Assumes that the script body has been already loaded and successully
-    // compiled and run, and all input arguments, and the function have been
-    // pushed to the stack.
-    // Returns 0 if successful. Otherwise returns non-zero Lua error code.
-    int Run();
-
-    // Updates stored listener and destroys the previous one.
-    void ResetListener(ScriptExecutorListener* listener);
-
-private:
-    lua_State* mLuaState;  // owned
-
-    std::unique_ptr<ScriptExecutorListener> mListener;
-};
-
-}  // namespace script_executor
-}  // namespace telemetry
-}  // namespace automotive
-}  // namespace android
-
-#endif  // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_LUAENGINE_H_
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
deleted file mode 100644
index 500b8e2..0000000
--- a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
+++ /dev/null
@@ -1,137 +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.
- */
-
-#include "JniUtils.h"
-#include "LuaEngine.h"
-#include "ScriptExecutorListener.h"
-#include "jni.h"
-
-#include <android-base/logging.h>
-
-#include <cstdint>
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-extern "C" {
-
-JNIEXPORT jlong JNICALL
-Java_com_android_car_telemetry_ScriptExecutor_nativeInitLuaEngine(JNIEnv* env, jobject object) {
-    // Cast first to intptr_t to ensure int can hold the pointer without loss.
-    return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
-}
-
-JNIEXPORT void JNICALL Java_com_android_car_telemetry_ScriptExecutor_nativeDestroyLuaEngine(
-        JNIEnv* env, jobject object, jlong luaEnginePtr) {
-    delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-}
-
-// Parses the inputs and loads them to Lua one at a time.
-// Loading of data into Lua also triggers checks on Lua side to verify the
-// inputs are valid. For example, pushing "functionName" into Lua stack verifies
-// that the function name actually exists in the previously loaded body of the
-// script.
-//
-// The steps are:
-// Step 1: Parse the inputs for obvious programming errors.
-// Step 2: Parse and load the body of the script.
-// Step 3: Parse and push function name we want to execute in the provided
-// script body to Lua stack. If the function name doesn't exist, we exit.
-// Step 4: Parse publishedData, convert it into Lua table and push it to the
-// stack.
-// Step 5: Parse savedState Bundle object, convert it into Lua table and push it
-// to the stack.
-// Any errors that occur at the stage above result in quick exit or crash.
-//
-// All interaction with Lua happens via Lua stack. Therefore, order of how the
-// inputs are parsed and processed is critical because Lua API methods such as
-// lua_pcall assume specific order between function name and the input arguments
-// on the stack.
-// More information about how to work with Lua stack: https://www.lua.org/pil/24.2.html
-// and how Lua functions are called via Lua API: https://www.lua.org/pil/25.2.html
-//
-// Finally, once parsing and pushing to Lua stack is complete, we do
-//
-// Step 6: attempt to run the provided function.
-JNIEXPORT void JNICALL Java_com_android_car_telemetry_ScriptExecutor_nativeInvokeScript(
-        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring scriptBody, jstring functionName,
-        jobject publishedData, jobject savedState, jobject listener) {
-    if (!luaEnginePtr) {
-        env->FatalError("luaEnginePtr parameter cannot be nil");
-    }
-    if (scriptBody == nullptr) {
-        env->FatalError("scriptBody parameter cannot be null");
-    }
-    if (functionName == nullptr) {
-        env->FatalError("functionName parameter cannot be null");
-    }
-    if (listener == nullptr) {
-        env->FatalError("listener parameter cannot be null");
-    }
-
-    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-
-    // Load and parse the script
-    const char* scriptStr = env->GetStringUTFChars(scriptBody, nullptr);
-    auto status = engine->LoadScript(scriptStr);
-    env->ReleaseStringUTFChars(scriptBody, scriptStr);
-    // status == 0 if the script loads successfully.
-    if (status) {
-        env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
-                      "Failed to load the script.");
-        return;
-    }
-    engine->ResetListener(new ScriptExecutorListener(env, listener));
-
-    // Push the function name we want to invoke to Lua stack
-    const char* functionNameStr = env->GetStringUTFChars(functionName, nullptr);
-    status = engine->PushFunction(functionNameStr);
-    env->ReleaseStringUTFChars(functionName, functionNameStr);
-    // status == 1 if the name is indeed a function.
-    if (!status) {
-        env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
-                      "symbol functionName does not correspond to a function.");
-        return;
-    }
-
-    // TODO(b/189241508): Provide implementation to parse publishedData input,
-    // convert it into Lua table and push into Lua stack.
-    if (publishedData) {
-        env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
-                      "Parsing of publishedData is not implemented yet.");
-        return;
-    }
-
-    // Unpack bundle in savedState, convert to Lua table and push it to Lua
-    // stack.
-    PushBundleToLuaTable(env, engine, savedState);
-
-    // Execute the function. This will block until complete or error.
-    if (engine->Run()) {
-        env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
-                      "Runtime error occurred while running the function.");
-        return;
-    }
-}
-
-}  // extern "C"
-
-}  // namespace script_executor
-}  // namespace telemetry
-}  // namespace automotive
-}  // namespace android
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
deleted file mode 100644
index 8c10aa4..0000000
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
+++ /dev/null
@@ -1,47 +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.
- */
-
-#include "ScriptExecutorListener.h"
-
-#include <android-base/logging.h>
-#include <android_runtime/AndroidRuntime.h>
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-ScriptExecutorListener::~ScriptExecutorListener() {
-    if (mScriptExecutorListener != NULL) {
-        JNIEnv* env = AndroidRuntime::getJNIEnv();
-        env->DeleteGlobalRef(mScriptExecutorListener);
-    }
-}
-
-ScriptExecutorListener::ScriptExecutorListener(JNIEnv* env, jobject script_executor_listener) {
-    mScriptExecutorListener = env->NewGlobalRef(script_executor_listener);
-}
-
-void ScriptExecutorListener::onError(const int errorType, const std::string& message,
-                                     const std::string& stackTrace) {
-    LOG(INFO) << "errorType: " << errorType << ", message: " << message
-              << ", stackTrace: " << stackTrace;
-}
-
-}  // namespace script_executor
-}  // namespace telemetry
-}  // namespace automotive
-}  // namespace android
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h b/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
deleted file mode 100644
index 1e5c7d7..0000000
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
+++ /dev/null
@@ -1,52 +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.
- */
-
-#ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
-#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
-
-#include "jni.h"
-
-#include <string>
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-//  Wrapper class for IScriptExecutorListener.aidl.
-class ScriptExecutorListener {
-public:
-    ScriptExecutorListener(JNIEnv* jni, jobject script_executor_listener);
-
-    virtual ~ScriptExecutorListener();
-
-    void onScriptFinished() {}
-
-    void onSuccess() {}
-
-    void onError(const int errorType, const std::string& message, const std::string& stackTrace);
-
-private:
-    // Stores a jni global reference to Java Script Executor listener object.
-    jobject mScriptExecutorListener;
-};
-
-}  // namespace script_executor
-}  // namespace telemetry
-}  // namespace automotive
-}  // namespace android
-
-#endif  // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
diff --git a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp b/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
deleted file mode 100644
index 9e2c43a..0000000
--- a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
+++ /dev/null
@@ -1,138 +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.
- */
-
-#include "JniUtils.h"
-#include "LuaEngine.h"
-#include "jni.h"
-
-#include <cstdint>
-#include <cstring>
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-namespace {
-
-extern "C" {
-
-#include "lua.h"
-
-JNIEXPORT jlong JNICALL
-Java_com_android_car_telemetry_JniUtilsTest_nativeCreateLuaEngine(JNIEnv* env, jobject object) {
-    // Cast first to intptr_t to ensure int can hold the pointer without loss.
-    return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
-}
-
-JNIEXPORT void JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeDestroyLuaEngine(
-        JNIEnv* env, jobject object, jlong luaEnginePtr) {
-    delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-}
-
-JNIEXPORT void JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativePushBundleToLuaTableCaller(
-        JNIEnv* env, jobject object, jlong luaEnginePtr, jobject bundle) {
-    PushBundleToLuaTable(env, reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr)),
-                         bundle);
-}
-
-JNIEXPORT jint JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeGetObjectSize(
-        JNIEnv* env, jobject object, jlong luaEnginePtr, jint index) {
-    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-    return lua_rawlen(engine->GetLuaState(), static_cast<int>(index));
-}
-
-JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasBooleanValue(
-        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jboolean value) {
-    const char* rawKey = env->GetStringUTFChars(key, nullptr);
-    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-    auto* luaState = engine->GetLuaState();
-    lua_pushstring(luaState, rawKey);
-    env->ReleaseStringUTFChars(key, rawKey);
-    lua_gettable(luaState, -2);
-    bool result = false;
-    if (!lua_isboolean(luaState, -1))
-        result = false;
-    else
-        result = static_cast<bool>(lua_toboolean(luaState, -1)) == static_cast<bool>(value);
-    lua_pop(luaState, 1);
-    return result;
-}
-
-JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasIntValue(
-        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jint value) {
-    const char* rawKey = env->GetStringUTFChars(key, nullptr);
-    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-    // Assumes the table is on top of the stack.
-    auto* luaState = engine->GetLuaState();
-    lua_pushstring(luaState, rawKey);
-    env->ReleaseStringUTFChars(key, rawKey);
-    lua_gettable(luaState, -2);
-    bool result = false;
-    if (!lua_isinteger(luaState, -1))
-        result = false;
-    else
-        result = lua_tointeger(luaState, -1) == static_cast<int>(value);
-    lua_pop(luaState, 1);
-    return result;
-}
-
-JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasDoubleValue(
-        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jdouble value) {
-    const char* rawKey = env->GetStringUTFChars(key, nullptr);
-    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-    // Assumes the table is on top of the stack.
-    auto* luaState = engine->GetLuaState();
-    lua_pushstring(luaState, rawKey);
-    env->ReleaseStringUTFChars(key, rawKey);
-    lua_gettable(luaState, -2);
-    bool result = false;
-    if (!lua_isnumber(luaState, -1))
-        result = false;
-    else
-        result = static_cast<double>(lua_tonumber(luaState, -1)) == static_cast<double>(value);
-    lua_pop(luaState, 1);
-    return result;
-}
-
-JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasStringValue(
-        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jstring value) {
-    const char* rawKey = env->GetStringUTFChars(key, nullptr);
-    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-    // Assumes the table is on top of the stack.
-    auto* luaState = engine->GetLuaState();
-    lua_pushstring(luaState, rawKey);
-    env->ReleaseStringUTFChars(key, rawKey);
-    lua_gettable(luaState, -2);
-    bool result = false;
-    if (!lua_isstring(luaState, -1)) {
-        result = false;
-    } else {
-        std::string s = lua_tostring(luaState, -1);
-        const char* rawValue = env->GetStringUTFChars(value, nullptr);
-        result = strcmp(lua_tostring(luaState, -1), rawValue) == 0;
-        env->ReleaseStringUTFChars(value, rawValue);
-    }
-    lua_pop(luaState, 1);
-    return result;
-}
-
-}  //  extern "C"
-
-}  //  namespace
-}  //  namespace script_executor
-}  //  namespace telemetry
-}  //  namespace automotive
-}  //  namespace android
diff --git a/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdog.aidl b/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdog.aidl
index 57d82ca..c9ece21 100644
--- a/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdog.aidl
+++ b/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdog.aidl
@@ -128,4 +128,12 @@
    * @param actions              List of actions take on resource overusing packages.
    */
    void actionTakenOnResourceOveruse(in List<PackageResourceOveruseAction> actions);
+
+   /**
+    * Enable/disable the internal client health check process.
+    * Disabling would stop the ANR killing process.
+    *
+    * @param isEnabled            New enabled state.
+    */
+    void controlProcessHealthCheck(in boolean disable);
 }
diff --git a/cpp/watchdog/car-watchdog-lib/src/android/car/watchdoglib/CarWatchdogDaemonHelper.java b/cpp/watchdog/car-watchdog-lib/src/android/car/watchdoglib/CarWatchdogDaemonHelper.java
index 1810c92..e7229ba 100644
--- a/cpp/watchdog/car-watchdog-lib/src/android/car/watchdoglib/CarWatchdogDaemonHelper.java
+++ b/cpp/watchdog/car-watchdog-lib/src/android/car/watchdoglib/CarWatchdogDaemonHelper.java
@@ -291,6 +291,16 @@
         invokeDaemonMethod((daemon) -> daemon.actionTakenOnResourceOveruse(actions));
     }
 
+    /**
+     * Enable/disable the internal client health check process.
+     * Disabling would stop the ANR killing process.
+     *
+     * @param disable True to disable watchdog's health check process.
+     */
+    public void controlProcessHealthCheck(boolean disable) throws RemoteException {
+        invokeDaemonMethod((daemon) -> daemon.controlProcessHealthCheck(disable));
+    }
+
     private void invokeDaemonMethod(Invokable r) throws RemoteException {
         ICarWatchdog daemon;
         synchronized (mLock) {
diff --git a/cpp/watchdog/sepolicy/private/carwatchdog.te b/cpp/watchdog/sepolicy/private/carwatchdog.te
index 91620f5..5b18ebf 100644
--- a/cpp/watchdog/sepolicy/private/carwatchdog.te
+++ b/cpp/watchdog/sepolicy/private/carwatchdog.te
@@ -1,4 +1,4 @@
-# Car watchdog server
+# Car watchdog server.
 typeattribute carwatchdogd coredomain;
 typeattribute carwatchdogd mlstrustedsubject;
 
@@ -9,22 +9,26 @@
 binder_use(carwatchdogd)
 binder_service(carwatchdogd)
 
-# Configration to communicate with VHAL
+# Configration to communicate with VHAL.
 hwbinder_use(carwatchdogd)
 get_prop(carwatchdogd, hwservicemanager_prop)
 hal_client_domain(carwatchdogd, hal_vehicle)
 
-# Scan through /proc/pid for all processes
+# Scan through /proc/pid for all processes.
 r_dir_file(carwatchdogd, domain)
 
-# Read /proc/uid_io/stats
+# Read /proc/uid_io/stats.
 allow carwatchdogd proc_uid_io_stats:file r_file_perms;
 
-# Read /proc/stat file
+# Read /proc/stat file.
 allow carwatchdogd proc_stat:file r_file_perms;
 
-# Read /proc/diskstats file
+# Read /proc/diskstats file.
 allow carwatchdogd proc_diskstats:file r_file_perms;
 
 # List HALs to get pid of vehicle HAL.
 allow carwatchdogd hwservicemanager:hwservice_manager list;
+
+# R/W /data/system/car for resource overuse configurations.
+allow carwatchdogd system_car_data_file:dir create_dir_perms;
+allow carwatchdogd system_car_data_file:{ file lnk_file } create_file_perms;
diff --git a/cpp/watchdog/sepolicy/public/carwatchdog.te b/cpp/watchdog/sepolicy/public/carwatchdog.te
index 2cb9c5a..fd7ab3b 100644
--- a/cpp/watchdog/sepolicy/public/carwatchdog.te
+++ b/cpp/watchdog/sepolicy/public/carwatchdog.te
@@ -1,9 +1,9 @@
-# Car watchdog server
+# Car watchdog server.
 type carwatchdogd, domain;
 
 binder_call(carwatchdogd, carwatchdogclient_domain)
 binder_call(carwatchdogclient_domain, carwatchdogd)
 
-# Configuration for system_server
+# Configuration for system_server.
 allow system_server carwatchdogd_service:service_manager find;
 binder_call(carwatchdogd, system_server)
diff --git a/cpp/watchdog/server/Android.bp b/cpp/watchdog/server/Android.bp
index 8ce4c04..c8d07b4 100644
--- a/cpp/watchdog/server/Android.bp
+++ b/cpp/watchdog/server/Android.bp
@@ -85,9 +85,10 @@
         "src/LooperWrapper.cpp",
         "src/OveruseConfigurationXmlHelper.cpp",
         "src/ProcDiskStats.cpp",
-        "src/ProcPidStat.cpp",
         "src/ProcStat.cpp",
-        "src/UidIoStats.cpp",
+        "src/UidIoStatsCollector.cpp",
+        "src/UidProcStatsCollector.cpp",
+        "src/UidStatsCollector.cpp",
     ],
     whole_static_libs: [
         "libwatchdog_properties",
@@ -120,11 +121,13 @@
         "tests/OveruseConfigurationTestUtils.cpp",
         "tests/OveruseConfigurationXmlHelperTest.cpp",
         "tests/PackageInfoResolverTest.cpp",
+        "tests/PackageInfoTestUtils.cpp",
         "tests/ProcDiskStatsTest.cpp",
         "tests/ProcPidDir.cpp",
-        "tests/ProcPidStatTest.cpp",
         "tests/ProcStatTest.cpp",
-        "tests/UidIoStatsTest.cpp",
+        "tests/UidIoStatsCollectorTest.cpp",
+        "tests/UidProcStatsCollectorTest.cpp",
+        "tests/UidStatsCollectorTest.cpp",
         "tests/WatchdogBinderMediatorTest.cpp",
         "tests/WatchdogInternalHandlerTest.cpp",
         "tests/WatchdogPerfServiceTest.cpp",
diff --git a/cpp/watchdog/server/src/IoOveruseConfigs.cpp b/cpp/watchdog/server/src/IoOveruseConfigs.cpp
index 6fb6633..0237e7d 100644
--- a/cpp/watchdog/server/src/IoOveruseConfigs.cpp
+++ b/cpp/watchdog/server/src/IoOveruseConfigs.cpp
@@ -83,12 +83,6 @@
     return output;
 }
 
-bool isZeroValueThresholds(const PerStateIoOveruseThreshold& thresholds) {
-    return thresholds.perStateWriteBytes.foregroundBytes == 0 &&
-            thresholds.perStateWriteBytes.backgroundBytes == 0 &&
-            thresholds.perStateWriteBytes.garageModeBytes == 0;
-}
-
 std::string toString(const PerStateIoOveruseThreshold& thresholds) {
     return StringPrintf("name=%s, foregroundBytes=%" PRId64 ", backgroundBytes=%" PRId64
                         ", garageModeBytes=%" PRId64,
@@ -102,23 +96,20 @@
         return Error() << "Doesn't contain threshold name";
     }
 
-    if (isZeroValueThresholds(thresholds)) {
-        return Error() << "Zero value thresholds for " << thresholds.name;
-    }
-
-    if (thresholds.perStateWriteBytes.foregroundBytes == 0 ||
-        thresholds.perStateWriteBytes.backgroundBytes == 0 ||
-        thresholds.perStateWriteBytes.garageModeBytes == 0) {
-        return Error() << "Some thresholds are zero: " << toString(thresholds);
+    if (thresholds.perStateWriteBytes.foregroundBytes <= 0 ||
+        thresholds.perStateWriteBytes.backgroundBytes <= 0 ||
+        thresholds.perStateWriteBytes.garageModeBytes <= 0) {
+        return Error() << "Some thresholds are less than or equal to zero: "
+                       << toString(thresholds);
     }
     return {};
 }
 
 Result<void> containsValidThreshold(const IoOveruseAlertThreshold& threshold) {
-    if (threshold.durationInSeconds == 0) {
+    if (threshold.durationInSeconds <= 0) {
         return Error() << "Duration must be greater than zero";
     }
-    if (threshold.writtenBytesPerSecond == 0) {
+    if (threshold.writtenBytesPerSecond <= 0) {
         return Error() << "Written bytes/second must be greater than zero";
     }
     return {};
@@ -238,6 +229,16 @@
     return {};
 }
 
+bool isSafeToKillAnyPackage(const std::vector<std::string>& packages,
+                            const std::unordered_set<std::string>& safeToKillPackages) {
+    for (const auto& packageName : packages) {
+        if (safeToKillPackages.find(packageName) != safeToKillPackages.end()) {
+            return true;
+        }
+    }
+    return false;
+}
+
 }  // namespace
 
 IoOveruseConfigs::ParseXmlFileFunction IoOveruseConfigs::sParseXmlFile =
@@ -579,7 +580,8 @@
     return {};
 }
 
-void IoOveruseConfigs::get(std::vector<ResourceOveruseConfiguration>* resourceOveruseConfigs) {
+void IoOveruseConfigs::get(
+        std::vector<ResourceOveruseConfiguration>* resourceOveruseConfigs) const {
     auto systemConfig = get(mSystemConfig, kSystemComponentUpdatableConfigs);
     if (systemConfig.has_value()) {
         systemConfig->componentType = ComponentType::SYSTEM;
@@ -600,7 +602,8 @@
 }
 
 std::optional<ResourceOveruseConfiguration> IoOveruseConfigs::get(
-        const ComponentSpecificConfig& componentSpecificConfig, const int32_t componentFilter) {
+        const ComponentSpecificConfig& componentSpecificConfig,
+        const int32_t componentFilter) const {
     if (componentSpecificConfig.mGeneric.name == kDefaultThresholdName) {
         return {};
     }
@@ -724,11 +727,26 @@
     }
     switch (packageInfo.componentType) {
         case ComponentType::SYSTEM:
-            return mSystemConfig.mSafeToKillPackages.find(packageInfo.packageIdentifier.name) !=
-                    mSystemConfig.mSafeToKillPackages.end();
+            if (mSystemConfig.mSafeToKillPackages.find(packageInfo.packageIdentifier.name) !=
+                mSystemConfig.mSafeToKillPackages.end()) {
+                return true;
+            }
+            return isSafeToKillAnyPackage(packageInfo.sharedUidPackages,
+                                          mSystemConfig.mSafeToKillPackages);
         case ComponentType::VENDOR:
-            return mVendorConfig.mSafeToKillPackages.find(packageInfo.packageIdentifier.name) !=
-                    mVendorConfig.mSafeToKillPackages.end();
+            if (mVendorConfig.mSafeToKillPackages.find(packageInfo.packageIdentifier.name) !=
+                mVendorConfig.mSafeToKillPackages.end()) {
+                return true;
+            }
+            /*
+             * Packages under the vendor shared UID may contain system packages because when
+             * CarWatchdogService derives the shared component type it attributes system packages
+             * as vendor packages when there is at least one vendor package.
+             */
+            return isSafeToKillAnyPackage(packageInfo.sharedUidPackages,
+                                          mSystemConfig.mSafeToKillPackages) ||
+                    isSafeToKillAnyPackage(packageInfo.sharedUidPackages,
+                                           mVendorConfig.mSafeToKillPackages);
         default:
             return true;
     }
diff --git a/cpp/watchdog/server/src/IoOveruseConfigs.h b/cpp/watchdog/server/src/IoOveruseConfigs.h
index 53ee9dd..7ae758b 100644
--- a/cpp/watchdog/server/src/IoOveruseConfigs.h
+++ b/cpp/watchdog/server/src/IoOveruseConfigs.h
@@ -68,7 +68,7 @@
 
 }  // namespace internal
 
-/*
+/**
  * Defines the methods that the I/O overuse configs module should implement.
  */
 class IIoOveruseConfigs : public android::RefBase {
@@ -80,18 +80,18 @@
     // Returns the existing configurations.
     virtual void get(
             std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*
-                    resourceOveruseConfigs) = 0;
+                    resourceOveruseConfigs) const = 0;
 
     // Writes the cached configs to disk.
     virtual android::base::Result<void> writeToDisk() = 0;
 
-    /*
+    /**
      * Returns the list of vendor package prefixes. Any pre-installed package matching one of these
      * prefixes should be classified as a vendor package.
      */
     virtual const std::unordered_set<std::string>& vendorPackagePrefixes() = 0;
 
-    /*
+    /**
      * Returns the package names to application category mappings.
      */
     virtual const std::unordered_map<
@@ -129,7 +129,7 @@
 
 class IoOveruseConfigs;
 
-/*
+/**
  * ComponentSpecificConfig represents the I/O overuse config defined per component.
  */
 class ComponentSpecificConfig final {
@@ -141,32 +141,32 @@
         mSafeToKillPackages.clear();
     }
 
-    /*
+    /**
      * Updates |mPerPackageThresholds|.
      */
     android::base::Result<void> updatePerPackageThresholds(
             const std::vector<android::automotive::watchdog::internal::PerStateIoOveruseThreshold>&
                     thresholds,
             const std::function<void(const std::string&)>& maybeAppendVendorPackagePrefixes);
-    /*
+    /**
      * Updates |mSafeToKillPackages|.
      */
     android::base::Result<void> updateSafeToKillPackages(
             const std::vector<std::string>& packages,
             const std::function<void(const std::string&)>& maybeAppendVendorPackagePrefixes);
 
-    /*
+    /**
      * I/O overuse configurations for all packages under the component that are not covered by
      * |mPerPackageThresholds| or |IoOveruseConfigs.mPerCategoryThresholds|.
      */
     android::automotive::watchdog::internal::PerStateIoOveruseThreshold mGeneric;
-    /*
+    /**
      * I/O overuse configurations for specific packages under the component.
      */
     std::unordered_map<std::string,
                        android::automotive::watchdog::internal::PerStateIoOveruseThreshold>
             mPerPackageThresholds;
-    /*
+    /**
      * List of safe to kill packages under the component in the event of I/O overuse.
      */
     std::unordered_set<std::string> mSafeToKillPackages;
@@ -175,7 +175,7 @@
     friend class IoOveruseConfigs;
 };
 
-/*
+/**
  * IoOveruseConfigs represents the I/O overuse configuration defined by system and vendor
  * applications. This class is not thread safe for performance purposes. The caller is responsible
  * for calling the methods in a thread safe manner.
@@ -194,7 +194,7 @@
                    configs) override;
 
     void get(std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*
-                     resourceOveruseConfigs) override;
+                     resourceOveruseConfigs) const override;
 
     android::base::Result<void> writeToDisk();
 
@@ -245,7 +245,8 @@
                     thresholds);
 
     std::optional<android::automotive::watchdog::internal::ResourceOveruseConfiguration> get(
-            const ComponentSpecificConfig& componentSpecificConfig, const int32_t componentFilter);
+            const ComponentSpecificConfig& componentSpecificConfig,
+            const int32_t componentFilter) const;
 
     // System component specific configuration.
     ComponentSpecificConfig mSystemConfig;
diff --git a/cpp/watchdog/server/src/IoOveruseMonitor.cpp b/cpp/watchdog/server/src/IoOveruseMonitor.cpp
index b97dd98..23e14ee 100644
--- a/cpp/watchdog/server/src/IoOveruseMonitor.cpp
+++ b/cpp/watchdog/server/src/IoOveruseMonitor.cpp
@@ -170,10 +170,11 @@
 }
 
 Result<void> IoOveruseMonitor::onPeriodicCollection(
-        time_t time, SystemState systemState, const android::wp<UidIoStats>& uidIoStats,
-        [[maybe_unused]] const android::wp<ProcStat>& procStat,
-        [[maybe_unused]] const android::wp<ProcPidStat>& procPidStat) {
-    if (uidIoStats == nullptr) {
+        time_t time, SystemState systemState,
+        const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+        [[maybe_unused]] const android::wp<ProcStat>& procStat) {
+    android::sp<UidStatsCollectorInterface> uidStatsCollectorSp = uidStatsCollector.promote();
+    if (uidStatsCollectorSp == nullptr) {
         return Error() << "Per-UID I/O stats collector must not be null";
     }
 
@@ -191,36 +192,24 @@
     mLastUserPackageIoMonitorTime = time;
     const auto [startTime, durationInSeconds] = calculateStartAndDuration(curGmt);
 
-    auto perUidIoUsage = uidIoStats.promote()->deltaStats();
-    /*
-     * TODO(b/185849350): Maybe move the packageInfo fetching logic into UidIoStats module.
-     *  This will also help avoid fetching package names in IoPerfCollection module.
-     */
-    std::vector<uid_t> seenUids;
-    for (auto it = perUidIoUsage.begin(); it != perUidIoUsage.end();) {
-        /*
-         * UidIoStats::deltaStats returns entries with zero write bytes because other metrics
-         * in these entries are non-zero.
-         */
-        if (it->second.ios.sumWriteBytes() == 0) {
-            it = perUidIoUsage.erase(it);
-            continue;
-        }
-        seenUids.push_back(it->first);
-        ++it;
-    }
-    if (perUidIoUsage.empty()) {
+    auto uidStats = uidStatsCollectorSp->deltaStats();
+    if (uidStats.empty()) {
         return {};
     }
-    const auto packageInfosByUid = mPackageInfoResolver->getPackageInfosForUids(seenUids);
     std::unordered_map<uid_t, IoOveruseStats> overusingNativeStats;
     bool isGarageModeActive = systemState == SystemState::GARAGE_MODE;
-    for (const auto& [uid, uidIoStats] : perUidIoUsage) {
-        const auto& packageInfo = packageInfosByUid.find(uid);
-        if (packageInfo == packageInfosByUid.end()) {
+    for (const auto& curUidStats : uidStats) {
+        if (curUidStats.ioStats.sumWriteBytes() == 0 || !curUidStats.hasPackageInfo()) {
+            /* 1. Ignore UIDs with zero written bytes since the last collection because they are
+             * either already accounted for or no writes made since system start.
+             *
+             * 2. UID stats without package info is not useful because the stats isn't attributed to
+             * any package/service.
+             */
             continue;
         }
-        UserPackageIoUsage curUsage(packageInfo->second, uidIoStats.ios, isGarageModeActive);
+        UserPackageIoUsage curUsage(curUidStats.packageInfo, curUidStats.ioStats,
+                                    isGarageModeActive);
         UserPackageIoUsage* dailyIoUsage;
         if (auto cachedUsage = mUserPackageDailyIoUsageById.find(curUsage.id());
             cachedUsage != mUserPackageDailyIoUsageById.end()) {
@@ -235,7 +224,7 @@
         const auto threshold = mIoOveruseConfigs->fetchThreshold(dailyIoUsage->packageInfo);
 
         PackageIoOveruseStats stats;
-        stats.uid = uid;
+        stats.uid = curUidStats.packageInfo.packageIdentifier.uid;
         stats.shouldNotify = false;
         stats.ioOveruseStats.startTime = startTime;
         stats.ioOveruseStats.durationInSeconds = durationInSeconds;
@@ -272,7 +261,7 @@
              */
             stats.shouldNotify = true;
             if (dailyIoUsage->packageInfo.uidType == UidType::NATIVE) {
-                overusingNativeStats[uid] = stats.ioOveruseStats;
+                overusingNativeStats[stats.uid] = stats.ioOveruseStats;
             }
             shouldSyncWatchdogService = true;
         } else if (dailyIoUsage->packageInfo.uidType != UidType::NATIVE &&
@@ -320,10 +309,10 @@
 Result<void> IoOveruseMonitor::onCustomCollection(
         time_t time, SystemState systemState,
         [[maybe_unused]] const std::unordered_set<std::string>& filterPackages,
-        const android::wp<UidIoStats>& uidIoStats, const android::wp<ProcStat>& procStat,
-        const android::wp<ProcPidStat>& procPidStat) {
+        const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+        const android::wp<ProcStat>& procStat) {
     // Nothing special for custom collection.
-    return onPeriodicCollection(time, systemState, uidIoStats, procStat, procPidStat);
+    return onPeriodicCollection(time, systemState, uidStatsCollector, procStat);
 }
 
 Result<void> IoOveruseMonitor::onPeriodicMonitor(
@@ -380,13 +369,13 @@
     return {};
 }
 
-Result<void> IoOveruseMonitor::onDump([[maybe_unused]] int fd) {
+Result<void> IoOveruseMonitor::onDump([[maybe_unused]] int fd) const {
     // TODO(b/183436216): Dump the list of killed/disabled packages. Dump the list of packages that
     //  exceed xx% of their threshold.
     return {};
 }
 
-bool IoOveruseMonitor::dumpHelpText(int fd) {
+bool IoOveruseMonitor::dumpHelpText(int fd) const {
     return WriteStringToFd(StringPrintf(kHelpText, name().c_str(), kResetResourceOveruseStatsFlag),
                            fd);
 }
@@ -438,7 +427,7 @@
 }
 
 Result<void> IoOveruseMonitor::getResourceOveruseConfigurations(
-        std::vector<ResourceOveruseConfiguration>* configs) {
+        std::vector<ResourceOveruseConfiguration>* configs) const {
     std::shared_lock readLock(mRwMutex);
     if (!isInitializedLocked()) {
         return Error(Status::EX_ILLEGAL_STATE) << name() << " is not initialized";
@@ -496,7 +485,7 @@
     return {};
 }
 
-Result<void> IoOveruseMonitor::getIoOveruseStats(IoOveruseStats* ioOveruseStats) {
+Result<void> IoOveruseMonitor::getIoOveruseStats(IoOveruseStats* ioOveruseStats) const {
     if (!isInitialized()) {
         return Error(Status::EX_ILLEGAL_STATE) << "I/O overuse monitor is not initialized";
     }
@@ -580,14 +569,14 @@
 }
 
 IoOveruseMonitor::UserPackageIoUsage::UserPackageIoUsage(const PackageInfo& pkgInfo,
-                                                         const IoUsage& ioUsage,
+                                                         const UidIoStats& uidIoStats,
                                                          const bool isGarageModeActive) {
     packageInfo = pkgInfo;
     if (isGarageModeActive) {
-        writtenBytes.garageModeBytes = ioUsage.sumWriteBytes();
+        writtenBytes.garageModeBytes = uidIoStats.sumWriteBytes();
     } else {
-        writtenBytes.foregroundBytes = ioUsage.metrics[WRITE_BYTES][FOREGROUND];
-        writtenBytes.backgroundBytes = ioUsage.metrics[WRITE_BYTES][BACKGROUND];
+        writtenBytes.foregroundBytes = uidIoStats.metrics[WRITE_BYTES][FOREGROUND];
+        writtenBytes.backgroundBytes = uidIoStats.metrics[WRITE_BYTES][BACKGROUND];
     }
 }
 
diff --git a/cpp/watchdog/server/src/IoOveruseMonitor.h b/cpp/watchdog/server/src/IoOveruseMonitor.h
index 27e179f..df7ff60 100644
--- a/cpp/watchdog/server/src/IoOveruseMonitor.h
+++ b/cpp/watchdog/server/src/IoOveruseMonitor.h
@@ -19,9 +19,8 @@
 
 #include "IoOveruseConfigs.h"
 #include "PackageInfoResolver.h"
-#include "ProcPidStat.h"
 #include "ProcStat.h"
-#include "UidIoStats.h"
+#include "UidStatsCollector.h"
 #include "WatchdogPerfService.h"
 
 #include <android-base/result.h>
@@ -62,17 +61,17 @@
 // Used only in tests.
 std::tuple<int64_t, int64_t> calculateStartAndDuration(const time_t& currentTime);
 
-/*
+/**
  * IIoOveruseMonitor interface defines the methods that the I/O overuse monitoring module
  * should implement.
  */
 class IIoOveruseMonitor : virtual public IDataProcessorInterface {
 public:
     // Returns whether or not the monitor is initialized.
-    virtual bool isInitialized() = 0;
+    virtual bool isInitialized() const = 0;
 
     // Dumps the help text.
-    virtual bool dumpHelpText(int fd) = 0;
+    virtual bool dumpHelpText(int fd) const = 0;
 
     // Below API is from internal/ICarWatchdog.aidl. Please refer to the AIDL for description.
     virtual android::base::Result<void> updateResourceOveruseConfigurations(
@@ -81,7 +80,7 @@
                     configs) = 0;
     virtual android::base::Result<void> getResourceOveruseConfigurations(
             std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*
-                    configs) = 0;
+                    configs) const = 0;
     virtual android::base::Result<void> actionTakenOnIoOveruse(
             const std::vector<
                     android::automotive::watchdog::internal::PackageResourceOveruseAction>&
@@ -94,7 +93,7 @@
     virtual android::base::Result<void> removeIoOveruseListener(
             const sp<IResourceOveruseListener>& listener) = 0;
 
-    virtual android::base::Result<void> getIoOveruseStats(IoOveruseStats* ioOveruseStats) = 0;
+    virtual android::base::Result<void> getIoOveruseStats(IoOveruseStats* ioOveruseStats) const = 0;
 
     virtual android::base::Result<void> resetIoOveruseStats(
             const std::vector<std::string>& packageNames) = 0;
@@ -106,42 +105,42 @@
 
     ~IoOveruseMonitor() { terminate(); }
 
-    bool isInitialized() {
+    bool isInitialized() const override {
         std::shared_lock readLock(mRwMutex);
         return isInitializedLocked();
     }
 
     // Below methods implement IDataProcessorInterface.
-    std::string name() { return "IoOveruseMonitor"; }
+    std::string name() const override { return "IoOveruseMonitor"; }
     friend std::ostream& operator<<(std::ostream& os, const IoOveruseMonitor& monitor);
     android::base::Result<void> onBoottimeCollection(
-            time_t /*time*/, const android::wp<UidIoStats>& /*uidIoStats*/,
-            const android::wp<ProcStat>& /*procStat*/,
-            const android::wp<ProcPidStat>& /*procPidStat*/) {
+            [[maybe_unused]] time_t time,
+            [[maybe_unused]] const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            [[maybe_unused]] const android::wp<ProcStat>& procStat) override {
         // No I/O overuse monitoring during boot-time.
         return {};
     }
 
-    android::base::Result<void> onPeriodicCollection(time_t time, SystemState systemState,
-                                                     const android::wp<UidIoStats>& uidIoStats,
-                                                     const android::wp<ProcStat>& procStat,
-                                                     const android::wp<ProcPidStat>& procPidStat);
+    android::base::Result<void> onPeriodicCollection(
+            time_t time, SystemState systemState,
+            const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::wp<ProcStat>& procStat) override;
 
     android::base::Result<void> onCustomCollection(
             time_t time, SystemState systemState,
             const std::unordered_set<std::string>& filterPackages,
-            const android::wp<UidIoStats>& uidIoStats, const android::wp<ProcStat>& procStat,
-            const android::wp<ProcPidStat>& procPidStat);
+            const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::wp<ProcStat>& procStat) override;
 
     android::base::Result<void> onPeriodicMonitor(
             time_t time, const android::wp<IProcDiskStatsInterface>& procDiskStats,
-            const std::function<void()>& alertHandler);
+            const std::function<void()>& alertHandler) override;
 
-    android::base::Result<void> onDump(int fd);
+    android::base::Result<void> onDump(int fd) const override;
 
-    bool dumpHelpText(int fd);
+    bool dumpHelpText(int fd) const override;
 
-    android::base::Result<void> onCustomCollectionDump(int /*fd*/) {
+    android::base::Result<void> onCustomCollectionDump([[maybe_unused]] int fd) override {
         // No special processing for custom collection. Thus no custom collection dump.
         return {};
     }
@@ -149,26 +148,28 @@
     // Below methods implement AIDL interfaces.
     android::base::Result<void> updateResourceOveruseConfigurations(
             const std::vector<
-                    android::automotive::watchdog::internal::ResourceOveruseConfiguration>&
-                    configs);
+                    android::automotive::watchdog::internal::ResourceOveruseConfiguration>& configs)
+            override;
 
     android::base::Result<void> getResourceOveruseConfigurations(
             std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*
-                    configs);
+                    configs) const override;
 
     android::base::Result<void> actionTakenOnIoOveruse(
             const std::vector<
-                    android::automotive::watchdog::internal::PackageResourceOveruseAction>&
-                    actions);
+                    android::automotive::watchdog::internal::PackageResourceOveruseAction>& actions)
+            override;
 
-    android::base::Result<void> addIoOveruseListener(const sp<IResourceOveruseListener>& listener);
+    android::base::Result<void> addIoOveruseListener(
+            const sp<IResourceOveruseListener>& listener) override;
 
     android::base::Result<void> removeIoOveruseListener(
-            const sp<IResourceOveruseListener>& listener);
+            const sp<IResourceOveruseListener>& listener) override;
 
-    android::base::Result<void> getIoOveruseStats(IoOveruseStats* ioOveruseStats);
+    android::base::Result<void> getIoOveruseStats(IoOveruseStats* ioOveruseStats) const override;
 
-    android::base::Result<void> resetIoOveruseStats(const std::vector<std::string>& packageName);
+    android::base::Result<void> resetIoOveruseStats(
+            const std::vector<std::string>& packageName) override;
 
 protected:
     android::base::Result<void> init();
@@ -183,7 +184,7 @@
 
     struct UserPackageIoUsage {
         UserPackageIoUsage(const android::automotive::watchdog::internal::PackageInfo& packageInfo,
-                           const IoUsage& IoUsage, const bool isGarageModeActive);
+                           const UidIoStats& uidIoStats, const bool isGarageModeActive);
         android::automotive::watchdog::internal::PackageInfo packageInfo = {};
         PerStateBytes writtenBytes = {};
         PerStateBytes forgivenWriteBytes = {};
@@ -211,7 +212,7 @@
     };
 
 private:
-    bool isInitializedLocked() { return mIoOveruseConfigs != nullptr; }
+    bool isInitializedLocked() const { return mIoOveruseConfigs != nullptr; }
 
     void notifyNativePackagesLocked(const std::unordered_map<uid_t, IoOveruseStats>& statsByUid);
 
@@ -221,7 +222,7 @@
     using Processor = std::function<void(ListenersByUidMap&, ListenersByUidMap::const_iterator)>;
     bool findListenerAndProcessLocked(const sp<IBinder>& binder, const Processor& processor);
 
-    /*
+    /**
      * Writes in-memory configs to disk asynchronously if configs are not written after latest
      * update.
      */
@@ -239,7 +240,7 @@
     // Summary of configs available for all the components and system-wide overuse alert thresholds.
     sp<IIoOveruseConfigs> mIoOveruseConfigs GUARDED_BY(mRwMutex);
 
-    /*
+    /**
      * Delta of system-wide written kib across all disks from the last |mPeriodicMonitorBufferSize|
      * polls along with the polling duration.
      */
diff --git a/cpp/watchdog/server/src/IoPerfCollection.cpp b/cpp/watchdog/server/src/IoPerfCollection.cpp
index 5537caf..d9f5e02 100644
--- a/cpp/watchdog/server/src/IoPerfCollection.cpp
+++ b/cpp/watchdog/server/src/IoPerfCollection.cpp
@@ -45,229 +45,281 @@
 
 namespace {
 
-const int32_t kDefaultTopNStatsPerCategory = 10;
-const int32_t kDefaultTopNStatsPerSubcategory = 5;
+constexpr int32_t kDefaultTopNStatsPerCategory = 10;
+constexpr int32_t kDefaultTopNStatsPerSubcategory = 5;
+constexpr const char kBootTimeCollectionTitle[] = "%s\nBoot-time I/O performance report:\n%s\n";
+constexpr const char kPeriodicCollectionTitle[] =
+        "%s\nLast N minutes I/O performance report:\n%s\n";
+constexpr const char kCustomCollectionTitle[] = "%s\nCustom I/O performance data report:\n%s\n";
+constexpr const char kCollectionTitle[] =
+        "Collection duration: %.f seconds\nNumber of collections: %zu\n";
+constexpr const char kRecordTitle[] = "\nCollection %zu: <%s>\n%s\n%s";
+constexpr const char kIoReadsTitle[] = "\nTop N Reads:\n%s\n";
+constexpr const char kIoWritesTitle[] = "\nTop N Writes:\n%s\n";
+constexpr const char kIoStatsHeader[] =
+        "Android User ID, Package Name, Foreground Bytes, Foreground Bytes %%, Foreground Fsync, "
+        "Foreground Fsync %%, Background Bytes, Background Bytes %%, Background Fsync, "
+        "Background Fsync %%\n";
+constexpr const char kIoBlockedTitle[] = "\nTop N I/O waiting UIDs:\n%s\n";
+constexpr const char kIoBlockedHeader[] =
+        "Android User ID, Package Name, Number of owned tasks waiting for I/O, Percentage of owned "
+        "tasks waiting for I/O\n\tCommand, Number of I/O waiting tasks, Percentage of UID's tasks "
+        "waiting for I/O\n";
+constexpr const char kMajorPageFaultsTitle[] = "\nTop N major page faults:\n%s\n";
+constexpr const char kMajorFaultsHeader[] =
+        "Android User ID, Package Name, Number of major page faults, Percentage of total major "
+        "page faults\n\tCommand, Number of major page faults, Percentage of UID's major page "
+        "faults\n";
+constexpr const char kMajorFaultsSummary[] =
+        "Number of major page faults since last collection: %" PRIu64 "\n"
+        "Percentage of change in major page faults since last collection: %.2f%%\n";
 
 double percentage(uint64_t numer, uint64_t denom) {
     return denom == 0 ? 0.0 : (static_cast<double>(numer) / static_cast<double>(denom)) * 100.0;
 }
 
-struct UidProcessStats {
-    struct ProcessInfo {
-        std::string comm = "";
-        uint64_t count = 0;
+void addUidIoStats(const int64_t entry[][UID_STATES], int64_t total[][UID_STATES]) {
+    const auto sum = [](int64_t lhs, int64_t rhs) -> int64_t {
+        return std::numeric_limits<int64_t>::max() - lhs > rhs
+                ? lhs + rhs
+                : std::numeric_limits<int64_t>::max();
     };
-    uint64_t uid = 0;
-    uint32_t ioBlockedTasksCnt = 0;
-    uint32_t totalTasksCnt = 0;
-    uint64_t majorFaults = 0;
-    std::vector<ProcessInfo> topNIoBlockedProcesses = {};
-    std::vector<ProcessInfo> topNMajorFaultProcesses = {};
-};
-
-std::unique_ptr<std::unordered_map<uid_t, UidProcessStats>> getUidProcessStats(
-        const std::vector<ProcessStats>& processStats, int topNStatsPerSubCategory) {
-    std::unique_ptr<std::unordered_map<uid_t, UidProcessStats>> uidProcessStats(
-            new std::unordered_map<uid_t, UidProcessStats>());
-    for (const auto& stats : processStats) {
-        if (stats.uid < 0) {
-            continue;
-        }
-        uid_t uid = static_cast<uid_t>(stats.uid);
-        if (uidProcessStats->find(uid) == uidProcessStats->end()) {
-            (*uidProcessStats)[uid] = UidProcessStats{
-                    .uid = uid,
-                    .topNIoBlockedProcesses = std::vector<
-                            UidProcessStats::ProcessInfo>(topNStatsPerSubCategory,
-                                                          UidProcessStats::ProcessInfo{}),
-                    .topNMajorFaultProcesses = std::vector<
-                            UidProcessStats::ProcessInfo>(topNStatsPerSubCategory,
-                                                          UidProcessStats::ProcessInfo{}),
-            };
-        }
-        auto& curUidProcessStats = (*uidProcessStats)[uid];
-        // Top-level process stats has the aggregated major page faults count and this should be
-        // persistent across thread creation/termination. Thus use the value from this field.
-        curUidProcessStats.majorFaults += stats.process.majorFaults;
-        curUidProcessStats.totalTasksCnt += stats.threads.size();
-        // The process state is the same as the main thread state. Thus to avoid double counting
-        // ignore the process state.
-        uint32_t ioBlockedTasksCnt = 0;
-        for (const auto& threadStat : stats.threads) {
-            ioBlockedTasksCnt += threadStat.second.state == "D" ? 1 : 0;
-        }
-        curUidProcessStats.ioBlockedTasksCnt += ioBlockedTasksCnt;
-        for (auto it = curUidProcessStats.topNIoBlockedProcesses.begin();
-             it != curUidProcessStats.topNIoBlockedProcesses.end(); ++it) {
-            if (it->count < ioBlockedTasksCnt) {
-                curUidProcessStats.topNIoBlockedProcesses
-                        .emplace(it,
-                                 UidProcessStats::ProcessInfo{
-                                         .comm = stats.process.comm,
-                                         .count = ioBlockedTasksCnt,
-                                 });
-                curUidProcessStats.topNIoBlockedProcesses.pop_back();
-                break;
-            }
-        }
-        for (auto it = curUidProcessStats.topNMajorFaultProcesses.begin();
-             it != curUidProcessStats.topNMajorFaultProcesses.end(); ++it) {
-            if (it->count < stats.process.majorFaults) {
-                curUidProcessStats.topNMajorFaultProcesses
-                        .emplace(it,
-                                 UidProcessStats::ProcessInfo{
-                                         .comm = stats.process.comm,
-                                         .count = stats.process.majorFaults,
-                                 });
-                curUidProcessStats.topNMajorFaultProcesses.pop_back();
-                break;
-            }
-        }
-    }
-    return uidProcessStats;
+    total[READ_BYTES][FOREGROUND] =
+            sum(total[READ_BYTES][FOREGROUND], entry[READ_BYTES][FOREGROUND]);
+    total[READ_BYTES][BACKGROUND] =
+            sum(total[READ_BYTES][BACKGROUND], entry[READ_BYTES][BACKGROUND]);
+    total[WRITE_BYTES][FOREGROUND] =
+            sum(total[WRITE_BYTES][FOREGROUND], entry[WRITE_BYTES][FOREGROUND]);
+    total[WRITE_BYTES][BACKGROUND] =
+            sum(total[WRITE_BYTES][BACKGROUND], entry[WRITE_BYTES][BACKGROUND]);
+    total[FSYNC_COUNT][FOREGROUND] =
+            sum(total[FSYNC_COUNT][FOREGROUND], entry[FSYNC_COUNT][FOREGROUND]);
+    total[FSYNC_COUNT][BACKGROUND] =
+            sum(total[FSYNC_COUNT][BACKGROUND], entry[FSYNC_COUNT][BACKGROUND]);
+    return;
 }
 
-Result<void> checkDataCollectors(const wp<UidIoStats>& uidIoStats, const wp<ProcStat>& procStat,
-                                 const wp<ProcPidStat>& procPidStat) {
-    if (uidIoStats != nullptr && procStat != nullptr && procPidStat != nullptr) {
+UserPackageStats toUserPackageStats(MetricType metricType, const UidStats& uidStats) {
+    const UidIoStats& ioStats = uidStats.ioStats;
+    return UserPackageStats{
+            .uid = uidStats.uid(),
+            .genericPackageName = uidStats.genericPackageName(),
+            .stats = UserPackageStats::
+                    IoStats{.bytes = {ioStats.metrics[metricType][UidState::FOREGROUND],
+                                      ioStats.metrics[metricType][UidState::BACKGROUND]},
+                            .fsync =
+                                    {ioStats.metrics[MetricType::FSYNC_COUNT][UidState::FOREGROUND],
+                                     ioStats.metrics[MetricType::FSYNC_COUNT]
+                                                    [UidState::BACKGROUND]}},
+    };
+}
+
+void cacheTopNIoStats(MetricType metricType, const UidStats& uidStats,
+                      std::vector<UserPackageStats>* topNIoStats) {
+    if (metricType != MetricType::READ_BYTES && metricType != MetricType::WRITE_BYTES) {
+        return;
+    }
+    int64_t totalBytes = metricType == MetricType::READ_BYTES ? uidStats.ioStats.sumReadBytes()
+                                                              : uidStats.ioStats.sumWriteBytes();
+    if (totalBytes == 0) {
+        return;
+    }
+    for (auto it = topNIoStats->begin(); it != topNIoStats->end(); ++it) {
+        if (const auto* ioStats = std::get_if<UserPackageStats::IoStats>(&it->stats);
+            ioStats == nullptr || totalBytes > ioStats->totalBytes()) {
+            topNIoStats->emplace(it, toUserPackageStats(metricType, uidStats));
+            topNIoStats->pop_back();
+            break;
+        }
+    }
+    return;
+}
+
+enum ProcStatType {
+    IO_BLOCKED_TASKS_COUNT = 0,
+    MAJOR_FAULTS,
+    PROC_STAT_TYPES,
+};
+
+bool cacheTopNProcessStats(ProcStatType procStatType, const ProcessStats& processStats,
+                           std::vector<UserPackageStats::ProcStats::ProcessCount>* topNProcesses) {
+    uint64_t count = procStatType == IO_BLOCKED_TASKS_COUNT ? processStats.ioBlockedTasksCount
+                                                            : processStats.totalMajorFaults;
+    if (count == 0) {
+        return false;
+    }
+    for (auto it = topNProcesses->begin(); it != topNProcesses->end(); ++it) {
+        if (count > it->count) {
+            topNProcesses->emplace(it,
+                                   UserPackageStats::ProcStats::ProcessCount{
+                                           .comm = processStats.comm,
+                                           .count = count,
+                                   });
+            topNProcesses->pop_back();
+            return true;
+        }
+    }
+    return false;
+}
+
+UserPackageStats toUserPackageStats(ProcStatType procStatType, const UidStats& uidStats,
+                                    int topNProcessCount) {
+    uint64_t count = procStatType == IO_BLOCKED_TASKS_COUNT ? uidStats.procStats.ioBlockedTasksCount
+                                                            : uidStats.procStats.totalMajorFaults;
+    UserPackageStats userPackageStats = {
+            .uid = uidStats.uid(),
+            .genericPackageName = uidStats.genericPackageName(),
+            .stats = UserPackageStats::ProcStats{.count = count},
+    };
+    auto& procStats = std::get<UserPackageStats::ProcStats>(userPackageStats.stats);
+    procStats.topNProcesses.resize(topNProcessCount);
+    int cachedProcessCount = 0;
+    for (const auto& [_, processStats] : uidStats.procStats.processStatsByPid) {
+        if (cacheTopNProcessStats(procStatType, processStats, &procStats.topNProcesses)) {
+            ++cachedProcessCount;
+        }
+    }
+    if (cachedProcessCount < topNProcessCount) {
+        procStats.topNProcesses.erase(procStats.topNProcesses.begin() + cachedProcessCount,
+                                      procStats.topNProcesses.end());
+    }
+    return userPackageStats;
+}
+
+bool cacheTopNProcStats(ProcStatType procStatType, const UidStats& uidStats, int topNProcessCount,
+                        std::vector<UserPackageStats>* topNProcStats) {
+    uint64_t count = procStatType == IO_BLOCKED_TASKS_COUNT ? uidStats.procStats.ioBlockedTasksCount
+                                                            : uidStats.procStats.totalMajorFaults;
+    if (count == 0) {
+        return false;
+    }
+    for (auto it = topNProcStats->begin(); it != topNProcStats->end(); ++it) {
+        if (const auto* procStats = std::get_if<UserPackageStats::ProcStats>(&it->stats);
+            procStats == nullptr || count > procStats->count) {
+            topNProcStats->emplace(it,
+                                   toUserPackageStats(procStatType, uidStats, topNProcessCount));
+            topNProcStats->pop_back();
+            return true;
+        }
+    }
+    return false;
+}
+
+Result<void> checkDataCollectors(const sp<UidStatsCollectorInterface>& uidStatsCollector,
+                                 const sp<ProcStat>& procStat) {
+    if (uidStatsCollector != nullptr && procStat != nullptr) {
         return {};
     }
     std::string error;
-    if (uidIoStats == nullptr) {
-        error = "Per-UID I/O stats collector must not be empty";
+    if (uidStatsCollector == nullptr) {
+        error = "Per-UID stats collector must not be null";
     }
     if (procStat == nullptr) {
         StringAppendF(&error, "%s%s", error.empty() ? "" : ", ",
-                      "Proc stats collector must not be empty");
+                      "Proc stats collector must not be null");
     }
-    if (procPidStat == nullptr) {
-        StringAppendF(&error, "%s%s", error.empty() ? "" : ", ",
-                      "Per-process stats collector must not be empty");
-    }
-
     return Error() << "Invalid data collectors: " << error;
 }
 
 }  // namespace
 
-std::string toString(const UidIoPerfData& data) {
+std::string UserPackageStats::toString(MetricType metricsType,
+                                       const int64_t totalIoStats[][UID_STATES]) const {
     std::string buffer;
-    if (data.topNReads.size() > 0) {
-        StringAppendF(&buffer, "\nTop N Reads:\n%s\n", std::string(12, '-').c_str());
-        StringAppendF(&buffer,
-                      "Android User ID, Package Name, Foreground Bytes, Foreground Bytes %%, "
-                      "Foreground Fsync, Foreground Fsync %%, Background Bytes, "
-                      "Background Bytes %%, Background Fsync, Background Fsync %%\n");
+    StringAppendF(&buffer, "%" PRIu32 ", %s", multiuser_get_user_id(uid),
+                  genericPackageName.c_str());
+    const auto& ioStats = std::get<UserPackageStats::IoStats>(stats);
+    for (int i = 0; i < UID_STATES; ++i) {
+        StringAppendF(&buffer, ", %" PRIi64 ", %.2f%%, %" PRIi64 ", %.2f%%", ioStats.bytes[i],
+                      percentage(ioStats.bytes[i], totalIoStats[metricsType][i]), ioStats.fsync[i],
+                      percentage(ioStats.fsync[i], totalIoStats[FSYNC_COUNT][i]));
     }
-    for (const auto& stat : data.topNReads) {
-        StringAppendF(&buffer, "%" PRIu32 ", %s", stat.userId, stat.packageName.c_str());
-        for (int i = 0; i < UID_STATES; ++i) {
-            StringAppendF(&buffer, ", %" PRIi64 ", %.2f%%, %" PRIi64 ", %.2f%%", stat.bytes[i],
-                          percentage(stat.bytes[i], data.total[READ_BYTES][i]), stat.fsync[i],
-                          percentage(stat.fsync[i], data.total[FSYNC_COUNT][i]));
-        }
-        StringAppendF(&buffer, "\n");
-    }
-    if (data.topNWrites.size() > 0) {
-        StringAppendF(&buffer, "\nTop N Writes:\n%s\n", std::string(13, '-').c_str());
-        StringAppendF(&buffer,
-                      "Android User ID, Package Name, Foreground Bytes, Foreground Bytes %%, "
-                      "Foreground Fsync, Foreground Fsync %%, Background Bytes, "
-                      "Background Bytes %%, Background Fsync, Background Fsync %%\n");
-    }
-    for (const auto& stat : data.topNWrites) {
-        StringAppendF(&buffer, "%" PRIu32 ", %s", stat.userId, stat.packageName.c_str());
-        for (int i = 0; i < UID_STATES; ++i) {
-            StringAppendF(&buffer, ", %" PRIi64 ", %.2f%%, %" PRIi64 ", %.2f%%", stat.bytes[i],
-                          percentage(stat.bytes[i], data.total[WRITE_BYTES][i]), stat.fsync[i],
-                          percentage(stat.fsync[i], data.total[FSYNC_COUNT][i]));
-        }
-        StringAppendF(&buffer, "\n");
+    StringAppendF(&buffer, "\n");
+    return buffer;
+}
+
+std::string UserPackageStats::toString(int64_t totalCount) const {
+    std::string buffer;
+    const auto& procStats = std::get<UserPackageStats::ProcStats>(stats);
+    StringAppendF(&buffer, "%" PRIu32 ", %s, %" PRIu64 ", %.2f%%\n", multiuser_get_user_id(uid),
+                  genericPackageName.c_str(), procStats.count,
+                  percentage(procStats.count, totalCount));
+    for (const auto& processCount : procStats.topNProcesses) {
+        StringAppendF(&buffer, "\t%s, %" PRIu64 ", %.2f%%\n", processCount.comm.c_str(),
+                      processCount.count, percentage(processCount.count, procStats.count));
     }
     return buffer;
 }
 
-std::string toString(const SystemIoPerfData& data) {
+std::string UserPackageSummaryStats::toString() const {
     std::string buffer;
-    StringAppendF(&buffer, "CPU I/O wait time/percent: %" PRIu64 " / %.2f%%\n", data.cpuIoWaitTime,
-                  percentage(data.cpuIoWaitTime, data.totalCpuTime));
+    if (!topNIoReads.empty()) {
+        StringAppendF(&buffer, kIoReadsTitle, std::string(12, '-').c_str());
+        StringAppendF(&buffer, kIoStatsHeader);
+        for (const auto& stats : topNIoReads) {
+            StringAppendF(&buffer, "%s",
+                          stats.toString(MetricType::READ_BYTES, totalIoStats).c_str());
+        }
+    }
+    if (!topNIoWrites.empty()) {
+        StringAppendF(&buffer, kIoWritesTitle, std::string(13, '-').c_str());
+        StringAppendF(&buffer, kIoStatsHeader);
+        for (const auto& stats : topNIoWrites) {
+            StringAppendF(&buffer, "%s",
+                          stats.toString(MetricType::WRITE_BYTES, totalIoStats).c_str());
+        }
+    }
+    if (!topNIoBlocked.empty()) {
+        StringAppendF(&buffer, kIoBlockedTitle, std::string(23, '-').c_str());
+        StringAppendF(&buffer, kIoBlockedHeader);
+        for (const auto& stats : topNIoBlocked) {
+            const auto it = taskCountByUid.find(stats.uid);
+            if (it == taskCountByUid.end()) {
+                continue;
+            }
+            StringAppendF(&buffer, "%s", stats.toString(it->second).c_str());
+        }
+    }
+    if (!topNMajorFaults.empty()) {
+        StringAppendF(&buffer, kMajorPageFaultsTitle, std::string(24, '-').c_str());
+        StringAppendF(&buffer, kMajorFaultsHeader);
+        for (const auto& stats : topNMajorFaults) {
+            StringAppendF(&buffer, "%s", stats.toString(totalMajorFaults).c_str());
+        }
+        StringAppendF(&buffer, kMajorFaultsSummary, totalMajorFaults, majorFaultsPercentChange);
+    }
+    return buffer;
+}
+
+std::string SystemSummaryStats::toString() const {
+    std::string buffer;
+    StringAppendF(&buffer, "CPU I/O wait time/percent: %" PRIu64 " / %.2f%%\n", cpuIoWaitTime,
+                  percentage(cpuIoWaitTime, totalCpuTime));
     StringAppendF(&buffer, "Number of I/O blocked processes/percent: %" PRIu32 " / %.2f%%\n",
-                  data.ioBlockedProcessesCnt,
-                  percentage(data.ioBlockedProcessesCnt, data.totalProcessesCnt));
+                  ioBlockedProcessCount, percentage(ioBlockedProcessCount, totalProcessCount));
     return buffer;
 }
 
-std::string toString(const ProcessIoPerfData& data) {
+std::string PerfStatsRecord::toString() const {
     std::string buffer;
-    StringAppendF(&buffer, "Number of major page faults since last collection: %" PRIu64 "\n",
-                  data.totalMajorFaults);
-    StringAppendF(&buffer,
-                  "Percentage of change in major page faults since last collection: %.2f%%\n",
-                  data.majorFaultsPercentChange);
-    if (data.topNMajorFaultUids.size() > 0) {
-        StringAppendF(&buffer, "\nTop N major page faults:\n%s\n", std::string(24, '-').c_str());
-        StringAppendF(&buffer,
-                      "Android User ID, Package Name, Number of major page faults, "
-                      "Percentage of total major page faults\n");
-        StringAppendF(&buffer,
-                      "\tCommand, Number of major page faults, Percentage of UID's major page "
-                      "faults\n");
-    }
-    for (const auto& uidStats : data.topNMajorFaultUids) {
-        StringAppendF(&buffer, "%" PRIu32 ", %s, %" PRIu64 ", %.2f%%\n", uidStats.userId,
-                      uidStats.packageName.c_str(), uidStats.count,
-                      percentage(uidStats.count, data.totalMajorFaults));
-        for (const auto& procStats : uidStats.topNProcesses) {
-            StringAppendF(&buffer, "\t%s, %" PRIu64 ", %.2f%%\n", procStats.comm.c_str(),
-                          procStats.count, percentage(procStats.count, uidStats.count));
-        }
-    }
-    if (data.topNIoBlockedUids.size() > 0) {
-        StringAppendF(&buffer, "\nTop N I/O waiting UIDs:\n%s\n", std::string(23, '-').c_str());
-        StringAppendF(&buffer,
-                      "Android User ID, Package Name, Number of owned tasks waiting for I/O, "
-                      "Percentage of owned tasks waiting for I/O\n");
-        StringAppendF(&buffer,
-                      "\tCommand, Number of I/O waiting tasks, Percentage of UID's tasks waiting "
-                      "for I/O\n");
-    }
-    for (size_t i = 0; i < data.topNIoBlockedUids.size(); ++i) {
-        const auto& uidStats = data.topNIoBlockedUids[i];
-        StringAppendF(&buffer, "%" PRIu32 ", %s, %" PRIu64 ", %.2f%%\n", uidStats.userId,
-                      uidStats.packageName.c_str(), uidStats.count,
-                      percentage(uidStats.count, data.topNIoBlockedUidsTotalTaskCnt[i]));
-        for (const auto& procStats : uidStats.topNProcesses) {
-            StringAppendF(&buffer, "\t%s, %" PRIu64 ", %.2f%%\n", procStats.comm.c_str(),
-                          procStats.count, percentage(procStats.count, uidStats.count));
-        }
-    }
+    StringAppendF(&buffer, "%s%s", systemSummaryStats.toString().c_str(),
+                  userPackageSummaryStats.toString().c_str());
     return buffer;
 }
 
-std::string toString(const IoPerfRecord& record) {
-    std::string buffer;
-    StringAppendF(&buffer, "%s%s%s", toString(record.systemIoPerfData).c_str(),
-                  toString(record.processIoPerfData).c_str(),
-                  toString(record.uidIoPerfData).c_str());
-    return buffer;
-}
-
-std::string toString(const CollectionInfo& collectionInfo) {
-    if (collectionInfo.records.empty()) {
+std::string CollectionInfo::toString() const {
+    if (records.empty()) {
         return kEmptyCollectionMessage;
     }
     std::string buffer;
-    double duration =
-            difftime(collectionInfo.records.back().time, collectionInfo.records.front().time);
-    StringAppendF(&buffer, "Collection duration: %.f seconds\nNumber of collections: %zu\n",
-                  duration, collectionInfo.records.size());
-
-    for (size_t i = 0; i < collectionInfo.records.size(); ++i) {
-        const auto& record = collectionInfo.records[i];
+    double duration = difftime(records.back().time, records.front().time);
+    StringAppendF(&buffer, kCollectionTitle, duration, records.size());
+    for (size_t i = 0; i < records.size(); ++i) {
+        const auto& record = records[i];
         std::stringstream timestamp;
         timestamp << std::put_time(std::localtime(&record.time), "%c %Z");
-        StringAppendF(&buffer, "\nCollection %zu: <%s>\n%s\n%s", i, timestamp.str().c_str(),
-                      std::string(45, '=').c_str(), toString(record).c_str());
+        StringAppendF(&buffer, kRecordTitle, i, timestamp.str().c_str(),
+                      std::string(45, '=').c_str(), record.toString().c_str());
     }
     return buffer;
 }
@@ -313,16 +365,16 @@
     mCustomCollection = {};
 }
 
-Result<void> IoPerfCollection::onDump(int fd) {
+Result<void> IoPerfCollection::onDump(int fd) const {
     Mutex::Autolock lock(mMutex);
-    if (!WriteStringToFd(StringPrintf("%s\nBoot-time I/O performance report:\n%s\n",
-                                      std::string(75, '-').c_str(), std::string(33, '=').c_str()),
+    if (!WriteStringToFd(StringPrintf(kBootTimeCollectionTitle, std::string(75, '-').c_str(),
+                                      std::string(33, '=').c_str()),
                          fd) ||
-        !WriteStringToFd(toString(mBoottimeCollection), fd) ||
-        !WriteStringToFd(StringPrintf("%s\nLast N minutes I/O performance report:\n%s\n",
-                                      std::string(75, '-').c_str(), std::string(38, '=').c_str()),
+        !WriteStringToFd(mBoottimeCollection.toString(), fd) ||
+        !WriteStringToFd(StringPrintf(kPeriodicCollectionTitle, std::string(75, '-').c_str(),
+                                      std::string(38, '=').c_str()),
                          fd) ||
-        !WriteStringToFd(toString(mPeriodicCollection), fd)) {
+        !WriteStringToFd(mPeriodicCollection.toString(), fd)) {
         return Error(FAILED_TRANSACTION)
                 << "Failed to dump the boot-time and periodic collection reports.";
     }
@@ -340,70 +392,70 @@
         return {};
     }
 
-    if (!WriteStringToFd(StringPrintf("%s\nCustom I/O performance data report:\n%s\n",
-                                      std::string(75, '-').c_str(), std::string(75, '-').c_str()),
+    if (!WriteStringToFd(StringPrintf(kCustomCollectionTitle, std::string(75, '-').c_str(),
+                                      std::string(75, '-').c_str()),
                          fd) ||
-        !WriteStringToFd(toString(mCustomCollection), fd)) {
+        !WriteStringToFd(mCustomCollection.toString(), fd)) {
         return Error(FAILED_TRANSACTION) << "Failed to write custom I/O collection report.";
     }
 
     return {};
 }
 
-Result<void> IoPerfCollection::onBoottimeCollection(time_t time, const wp<UidIoStats>& uidIoStats,
-                                                    const wp<ProcStat>& procStat,
-                                                    const wp<ProcPidStat>& procPidStat) {
-    auto result = checkDataCollectors(uidIoStats, procStat, procPidStat);
+Result<void> IoPerfCollection::onBoottimeCollection(
+        time_t time, const wp<UidStatsCollectorInterface>& uidStatsCollector,
+        const wp<ProcStat>& procStat) {
+    const sp<UidStatsCollectorInterface> uidStatsCollectorSp = uidStatsCollector.promote();
+    const sp<ProcStat> procStatSp = procStat.promote();
+    auto result = checkDataCollectors(uidStatsCollectorSp, procStatSp);
     if (!result.ok()) {
         return result;
     }
     Mutex::Autolock lock(mMutex);
-    return processLocked(time, std::unordered_set<std::string>(), uidIoStats, procStat, procPidStat,
+    return processLocked(time, std::unordered_set<std::string>(), uidStatsCollectorSp, procStatSp,
                          &mBoottimeCollection);
 }
 
-Result<void> IoPerfCollection::onPeriodicCollection(time_t time,
-                                                    [[maybe_unused]] SystemState systemState,
-                                                    const wp<UidIoStats>& uidIoStats,
-                                                    const wp<ProcStat>& procStat,
-                                                    const wp<ProcPidStat>& procPidStat) {
-    auto result = checkDataCollectors(uidIoStats, procStat, procPidStat);
+Result<void> IoPerfCollection::onPeriodicCollection(
+        time_t time, [[maybe_unused]] SystemState systemState,
+        const wp<UidStatsCollectorInterface>& uidStatsCollector, const wp<ProcStat>& procStat) {
+    const sp<UidStatsCollectorInterface> uidStatsCollectorSp = uidStatsCollector.promote();
+    const sp<ProcStat> procStatSp = procStat.promote();
+    auto result = checkDataCollectors(uidStatsCollectorSp, procStatSp);
     if (!result.ok()) {
         return result;
     }
     Mutex::Autolock lock(mMutex);
-    return processLocked(time, std::unordered_set<std::string>(), uidIoStats, procStat, procPidStat,
+    return processLocked(time, std::unordered_set<std::string>(), uidStatsCollectorSp, procStatSp,
                          &mPeriodicCollection);
 }
 
 Result<void> IoPerfCollection::onCustomCollection(
         time_t time, [[maybe_unused]] SystemState systemState,
-        const std::unordered_set<std::string>& filterPackages, const wp<UidIoStats>& uidIoStats,
-        const wp<ProcStat>& procStat, const wp<ProcPidStat>& procPidStat) {
-    auto result = checkDataCollectors(uidIoStats, procStat, procPidStat);
+        const std::unordered_set<std::string>& filterPackages,
+        const wp<UidStatsCollectorInterface>& uidStatsCollector, const wp<ProcStat>& procStat) {
+    const sp<UidStatsCollectorInterface> uidStatsCollectorSp = uidStatsCollector.promote();
+    const sp<ProcStat> procStatSp = procStat.promote();
+    auto result = checkDataCollectors(uidStatsCollectorSp, procStatSp);
     if (!result.ok()) {
         return result;
     }
     Mutex::Autolock lock(mMutex);
-    return processLocked(time, filterPackages, uidIoStats, procStat, procPidStat,
-                         &mCustomCollection);
+    return processLocked(time, filterPackages, uidStatsCollectorSp, procStatSp, &mCustomCollection);
 }
 
-Result<void> IoPerfCollection::processLocked(time_t time,
-                                             const std::unordered_set<std::string>& filterPackages,
-                                             const wp<UidIoStats>& uidIoStats,
-                                             const wp<ProcStat>& procStat,
-                                             const wp<ProcPidStat>& procPidStat,
-                                             CollectionInfo* collectionInfo) {
+Result<void> IoPerfCollection::processLocked(
+        time_t time, const std::unordered_set<std::string>& filterPackages,
+        const sp<UidStatsCollectorInterface>& uidStatsCollector, const sp<ProcStat>& procStat,
+        CollectionInfo* collectionInfo) {
     if (collectionInfo->maxCacheSize == 0) {
         return Error() << "Maximum cache size cannot be 0";
     }
-    IoPerfRecord record{
+    PerfStatsRecord record{
             .time = time,
     };
-    processSystemIoPerfData(procStat, &record.systemIoPerfData);
-    processProcessIoPerfDataLocked(filterPackages, procPidStat, &record.processIoPerfData);
-    processUidIoPerfData(filterPackages, uidIoStats, &record.uidIoPerfData);
+    processUidStatsLocked(filterPackages, uidStatsCollector, &record.userPackageSummaryStats);
+    processProcStatLocked(procStat, &record.systemSummaryStats);
     if (collectionInfo->records.size() > collectionInfo->maxCacheSize) {
         collectionInfo->records.erase(collectionInfo->records.begin());  // Erase the oldest record.
     }
@@ -411,220 +463,82 @@
     return {};
 }
 
-void IoPerfCollection::processUidIoPerfData(const std::unordered_set<std::string>& filterPackages,
-                                            const wp<UidIoStats>& uidIoStats,
-                                            UidIoPerfData* uidIoPerfData) const {
-    const std::unordered_map<uid_t, UidIoUsage>& usages = uidIoStats.promote()->deltaStats();
-
-    // Fetch only the top N reads and writes from the usage records.
-    UidIoUsage tempUsage = {};
-    std::vector<const UidIoUsage*> topNReads(mTopNStatsPerCategory, &tempUsage);
-    std::vector<const UidIoUsage*> topNWrites(mTopNStatsPerCategory, &tempUsage);
-    std::vector<uid_t> uids;
-
-    for (const auto& uIt : usages) {
-        const UidIoUsage& curUsage = uIt.second;
-        uids.push_back(curUsage.uid);
-        uidIoPerfData->total[READ_BYTES][FOREGROUND] +=
-                curUsage.ios.metrics[READ_BYTES][FOREGROUND];
-        uidIoPerfData->total[READ_BYTES][BACKGROUND] +=
-                curUsage.ios.metrics[READ_BYTES][BACKGROUND];
-        uidIoPerfData->total[WRITE_BYTES][FOREGROUND] +=
-                curUsage.ios.metrics[WRITE_BYTES][FOREGROUND];
-        uidIoPerfData->total[WRITE_BYTES][BACKGROUND] +=
-                curUsage.ios.metrics[WRITE_BYTES][BACKGROUND];
-        uidIoPerfData->total[FSYNC_COUNT][FOREGROUND] +=
-                curUsage.ios.metrics[FSYNC_COUNT][FOREGROUND];
-        uidIoPerfData->total[FSYNC_COUNT][BACKGROUND] +=
-                curUsage.ios.metrics[FSYNC_COUNT][BACKGROUND];
-
-        for (auto it = topNReads.begin(); it != topNReads.end(); ++it) {
-            const UidIoUsage* curRead = *it;
-            if (curRead->ios.sumReadBytes() < curUsage.ios.sumReadBytes()) {
-                topNReads.emplace(it, &curUsage);
-                if (filterPackages.empty()) {
-                    topNReads.pop_back();
-                }
-                break;
-            }
-        }
-        for (auto it = topNWrites.begin(); it != topNWrites.end(); ++it) {
-            const UidIoUsage* curWrite = *it;
-            if (curWrite->ios.sumWriteBytes() < curUsage.ios.sumWriteBytes()) {
-                topNWrites.emplace(it, &curUsage);
-                if (filterPackages.empty()) {
-                    topNWrites.pop_back();
-                }
-                break;
-            }
-        }
+void IoPerfCollection::processUidStatsLocked(
+        const std::unordered_set<std::string>& filterPackages,
+        const sp<UidStatsCollectorInterface>& uidStatsCollector,
+        UserPackageSummaryStats* userPackageSummaryStats) {
+    const std::vector<UidStats>& uidStats = uidStatsCollector->deltaStats();
+    if (uidStats.empty()) {
+        return;
     }
-
-    const auto& uidToPackageNameMapping = mPackageInfoResolver->getPackageNamesForUids(uids);
-
-    // Convert the top N I/O usage to UidIoPerfData.
-    for (const auto& usage : topNReads) {
-        if (usage->ios.isZero()) {
-            // End of non-zero usage records. This case occurs when the number of UIDs with active
-            // I/O operations is < |ro.carwatchdog.top_n_stats_per_category|.
-            break;
-        }
-        UidIoPerfData::Stats stats = {
-                .userId = multiuser_get_user_id(usage->uid),
-                .packageName = std::to_string(usage->uid),
-                .bytes = {usage->ios.metrics[READ_BYTES][FOREGROUND],
-                          usage->ios.metrics[READ_BYTES][BACKGROUND]},
-                .fsync = {usage->ios.metrics[FSYNC_COUNT][FOREGROUND],
-                          usage->ios.metrics[FSYNC_COUNT][BACKGROUND]},
-        };
-        if (uidToPackageNameMapping.find(usage->uid) != uidToPackageNameMapping.end()) {
-            stats.packageName = uidToPackageNameMapping.at(usage->uid);
-        }
-        if (!filterPackages.empty() &&
-            filterPackages.find(stats.packageName) == filterPackages.end()) {
+    if (filterPackages.empty()) {
+        userPackageSummaryStats->topNIoReads.resize(mTopNStatsPerCategory);
+        userPackageSummaryStats->topNIoWrites.resize(mTopNStatsPerCategory);
+        userPackageSummaryStats->topNIoBlocked.resize(mTopNStatsPerCategory);
+        userPackageSummaryStats->topNMajorFaults.resize(mTopNStatsPerCategory);
+    }
+    for (const auto& curUidStats : uidStats) {
+        uid_t uid = curUidStats.uid();
+        addUidIoStats(curUidStats.ioStats.metrics, userPackageSummaryStats->totalIoStats);
+        userPackageSummaryStats->totalMajorFaults += curUidStats.procStats.totalMajorFaults;
+        if (filterPackages.empty()) {
+            cacheTopNIoStats(MetricType::READ_BYTES, curUidStats,
+                             &userPackageSummaryStats->topNIoReads);
+            cacheTopNIoStats(MetricType::WRITE_BYTES, curUidStats,
+                             &userPackageSummaryStats->topNIoWrites);
+            if (cacheTopNProcStats(IO_BLOCKED_TASKS_COUNT, curUidStats, mTopNStatsPerSubcategory,
+                                   &userPackageSummaryStats->topNIoBlocked)) {
+                userPackageSummaryStats->taskCountByUid[uid] =
+                        curUidStats.procStats.totalTasksCount;
+            }
+            cacheTopNProcStats(MAJOR_FAULTS, curUidStats, mTopNStatsPerSubcategory,
+                               &userPackageSummaryStats->topNMajorFaults);
             continue;
         }
-        uidIoPerfData->topNReads.emplace_back(stats);
-    }
-
-    for (const auto& usage : topNWrites) {
-        if (usage->ios.isZero()) {
-            // End of non-zero usage records. This case occurs when the number of UIDs with active
-            // I/O operations is < |ro.carwatchdog.top_n_stats_per_category|.
-            break;
-        }
-        UidIoPerfData::Stats stats = {
-                .userId = multiuser_get_user_id(usage->uid),
-                .packageName = std::to_string(usage->uid),
-                .bytes = {usage->ios.metrics[WRITE_BYTES][FOREGROUND],
-                          usage->ios.metrics[WRITE_BYTES][BACKGROUND]},
-                .fsync = {usage->ios.metrics[FSYNC_COUNT][FOREGROUND],
-                          usage->ios.metrics[FSYNC_COUNT][BACKGROUND]},
-        };
-        if (uidToPackageNameMapping.find(usage->uid) != uidToPackageNameMapping.end()) {
-            stats.packageName = uidToPackageNameMapping.at(usage->uid);
-        }
-        if (!filterPackages.empty() &&
-            filterPackages.find(stats.packageName) == filterPackages.end()) {
-            continue;
-        }
-        uidIoPerfData->topNWrites.emplace_back(stats);
-    }
-}
-
-void IoPerfCollection::processSystemIoPerfData(const wp<ProcStat>& procStat,
-                                               SystemIoPerfData* systemIoPerfData) const {
-    const ProcStatInfo& procStatInfo = procStat.promote()->deltaStats();
-    systemIoPerfData->cpuIoWaitTime = procStatInfo.cpuStats.ioWaitTime;
-    systemIoPerfData->totalCpuTime = procStatInfo.totalCpuTime();
-    systemIoPerfData->ioBlockedProcessesCnt = procStatInfo.ioBlockedProcessesCnt;
-    systemIoPerfData->totalProcessesCnt = procStatInfo.totalProcessesCnt();
-}
-
-void IoPerfCollection::processProcessIoPerfDataLocked(
-        const std::unordered_set<std::string>& filterPackages, const wp<ProcPidStat>& procPidStat,
-        ProcessIoPerfData* processIoPerfData) {
-    const std::vector<ProcessStats>& processStats = procPidStat.promote()->deltaStats();
-
-    const auto& uidProcessStats = getUidProcessStats(processStats, mTopNStatsPerSubcategory);
-    std::vector<uid_t> uids;
-    // Fetch only the top N I/O blocked UIDs and UIDs with most major page faults.
-    UidProcessStats temp = {};
-    std::vector<const UidProcessStats*> topNIoBlockedUids(mTopNStatsPerCategory, &temp);
-    std::vector<const UidProcessStats*> topNMajorFaultUids(mTopNStatsPerCategory, &temp);
-    processIoPerfData->totalMajorFaults = 0;
-    for (const auto& it : *uidProcessStats) {
-        const UidProcessStats& curStats = it.second;
-        uids.push_back(curStats.uid);
-        processIoPerfData->totalMajorFaults += curStats.majorFaults;
-        for (auto it = topNIoBlockedUids.begin(); it != topNIoBlockedUids.end(); ++it) {
-            const UidProcessStats* topStats = *it;
-            if (topStats->ioBlockedTasksCnt < curStats.ioBlockedTasksCnt) {
-                topNIoBlockedUids.emplace(it, &curStats);
-                if (filterPackages.empty()) {
-                    topNIoBlockedUids.pop_back();
-                }
-                break;
-            }
-        }
-        for (auto it = topNMajorFaultUids.begin(); it != topNMajorFaultUids.end(); ++it) {
-            const UidProcessStats* topStats = *it;
-            if (topStats->majorFaults < curStats.majorFaults) {
-                topNMajorFaultUids.emplace(it, &curStats);
-                if (filterPackages.empty()) {
-                    topNMajorFaultUids.pop_back();
-                }
-                break;
-            }
+        if (filterPackages.count(curUidStats.genericPackageName()) != 0) {
+            userPackageSummaryStats->topNIoReads.emplace_back(
+                    toUserPackageStats(MetricType::READ_BYTES, curUidStats));
+            userPackageSummaryStats->topNIoWrites.emplace_back(
+                    toUserPackageStats(MetricType::WRITE_BYTES, curUidStats));
+            userPackageSummaryStats->topNIoBlocked.emplace_back(
+                    toUserPackageStats(IO_BLOCKED_TASKS_COUNT, curUidStats,
+                                       mTopNStatsPerSubcategory));
+            userPackageSummaryStats->topNMajorFaults.emplace_back(
+                    toUserPackageStats(MAJOR_FAULTS, curUidStats, mTopNStatsPerSubcategory));
+            userPackageSummaryStats->taskCountByUid[uid] = curUidStats.procStats.totalTasksCount;
         }
     }
-
-    const auto& uidToPackageNameMapping = mPackageInfoResolver->getPackageNamesForUids(uids);
-
-    // Convert the top N uid process stats to ProcessIoPerfData.
-    for (const auto& it : topNIoBlockedUids) {
-        if (it->ioBlockedTasksCnt == 0) {
-            // End of non-zero elements. This case occurs when the number of UIDs with I/O blocked
-            // processes is < |ro.carwatchdog.top_n_stats_per_category|.
-            break;
-        }
-        ProcessIoPerfData::UidStats stats = {
-                .userId = multiuser_get_user_id(it->uid),
-                .packageName = std::to_string(it->uid),
-                .count = it->ioBlockedTasksCnt,
-        };
-        if (uidToPackageNameMapping.find(it->uid) != uidToPackageNameMapping.end()) {
-            stats.packageName = uidToPackageNameMapping.at(it->uid);
-        }
-        if (!filterPackages.empty() &&
-            filterPackages.find(stats.packageName) == filterPackages.end()) {
-            continue;
-        }
-        for (const auto& pIt : it->topNIoBlockedProcesses) {
-            if (pIt.count == 0) {
-                break;
-            }
-            stats.topNProcesses.emplace_back(
-                    ProcessIoPerfData::UidStats::ProcessStats{pIt.comm, pIt.count});
-        }
-        processIoPerfData->topNIoBlockedUids.emplace_back(stats);
-        processIoPerfData->topNIoBlockedUidsTotalTaskCnt.emplace_back(it->totalTasksCnt);
-    }
-    for (const auto& it : topNMajorFaultUids) {
-        if (it->majorFaults == 0) {
-            // End of non-zero elements. This case occurs when the number of UIDs with major faults
-            // is < |ro.carwatchdog.top_n_stats_per_category|.
-            break;
-        }
-        ProcessIoPerfData::UidStats stats = {
-                .userId = multiuser_get_user_id(it->uid),
-                .packageName = std::to_string(it->uid),
-                .count = it->majorFaults,
-        };
-        if (uidToPackageNameMapping.find(it->uid) != uidToPackageNameMapping.end()) {
-            stats.packageName = uidToPackageNameMapping.at(it->uid);
-        }
-        if (!filterPackages.empty() &&
-            filterPackages.find(stats.packageName) == filterPackages.end()) {
-            continue;
-        }
-        for (const auto& pIt : it->topNMajorFaultProcesses) {
-            if (pIt.count == 0) {
-                break;
-            }
-            stats.topNProcesses.emplace_back(
-                    ProcessIoPerfData::UidStats::ProcessStats{pIt.comm, pIt.count});
-        }
-        processIoPerfData->topNMajorFaultUids.emplace_back(stats);
-    }
-    if (mLastMajorFaults == 0) {
-        processIoPerfData->majorFaultsPercentChange = 0;
-    } else {
-        int64_t increase = processIoPerfData->totalMajorFaults - mLastMajorFaults;
-        processIoPerfData->majorFaultsPercentChange =
+    if (mLastMajorFaults != 0) {
+        int64_t increase = userPackageSummaryStats->totalMajorFaults - mLastMajorFaults;
+        userPackageSummaryStats->majorFaultsPercentChange =
                 (static_cast<double>(increase) / static_cast<double>(mLastMajorFaults)) * 100.0;
     }
-    mLastMajorFaults = processIoPerfData->totalMajorFaults;
+    mLastMajorFaults = userPackageSummaryStats->totalMajorFaults;
+
+    const auto removeEmptyStats = [](std::vector<UserPackageStats>& userPackageStats) {
+        for (auto it = userPackageStats.begin(); it != userPackageStats.end(); ++it) {
+            /* std::monostate is the first alternative in the variant. When the variant is
+             * uninitialized, the index points to this alternative.
+             */
+            if (it->stats.index() == 0) {
+                userPackageStats.erase(it, userPackageStats.end());
+                break;
+            }
+        }
+    };
+    removeEmptyStats(userPackageSummaryStats->topNIoReads);
+    removeEmptyStats(userPackageSummaryStats->topNIoWrites);
+    removeEmptyStats(userPackageSummaryStats->topNIoBlocked);
+    removeEmptyStats(userPackageSummaryStats->topNMajorFaults);
+}
+
+void IoPerfCollection::processProcStatLocked(const sp<ProcStat>& procStat,
+                                             SystemSummaryStats* systemSummaryStats) const {
+    const ProcStatInfo& procStatInfo = procStat->deltaStats();
+    systemSummaryStats->cpuIoWaitTime = procStatInfo.cpuStats.ioWaitTime;
+    systemSummaryStats->totalCpuTime = procStatInfo.totalCpuTime();
+    systemSummaryStats->ioBlockedProcessCount = procStatInfo.ioBlockedProcessCount;
+    systemSummaryStats->totalProcessCount = procStatInfo.totalProcessCount();
 }
 
 }  // namespace watchdog
diff --git a/cpp/watchdog/server/src/IoPerfCollection.h b/cpp/watchdog/server/src/IoPerfCollection.h
index 2e976da..265cb55 100644
--- a/cpp/watchdog/server/src/IoPerfCollection.h
+++ b/cpp/watchdog/server/src/IoPerfCollection.h
@@ -17,11 +17,9 @@
 #ifndef CPP_WATCHDOG_SERVER_SRC_IOPERFCOLLECTION_H_
 #define CPP_WATCHDOG_SERVER_SRC_IOPERFCOLLECTION_H_
 
-#include "PackageInfoResolver.h"
 #include "ProcDiskStats.h"
-#include "ProcPidStat.h"
 #include "ProcStat.h"
-#include "UidIoStats.h"
+#include "UidStatsCollector.h"
 #include "WatchdogPerfService.h"
 
 #include <android-base/result.h>
@@ -34,79 +32,16 @@
 #include <ctime>
 #include <string>
 #include <unordered_set>
+#include <variant>
 #include <vector>
 
 namespace android {
 namespace automotive {
 namespace watchdog {
 
-// Number of periodic collection perf data snapshots to cache in memory.
-const int32_t kDefaultPeriodicCollectionBufferSize = 180;
-constexpr const char* kEmptyCollectionMessage = "No collection recorded\n";
-
-// Performance data collected from the `/proc/uid_io/stats` file.
-struct UidIoPerfData {
-    struct Stats {
-        userid_t userId = 0;
-        std::string packageName;
-        int64_t bytes[UID_STATES];
-        int64_t fsync[UID_STATES];
-    };
-    std::vector<Stats> topNReads = {};
-    std::vector<Stats> topNWrites = {};
-    int64_t total[METRIC_TYPES][UID_STATES] = {{0}};
-};
-
-std::string toString(const UidIoPerfData& perfData);
-
-// Performance data collected from the `/proc/stats` file.
-struct SystemIoPerfData {
-    uint64_t cpuIoWaitTime = 0;
-    uint64_t totalCpuTime = 0;
-    uint32_t ioBlockedProcessesCnt = 0;
-    uint32_t totalProcessesCnt = 0;
-};
-
-std::string toString(const SystemIoPerfData& perfData);
-
-// Performance data collected from the `/proc/[pid]/stat` and `/proc/[pid]/task/[tid]/stat` files.
-struct ProcessIoPerfData {
-    struct UidStats {
-        userid_t userId = 0;
-        std::string packageName;
-        uint64_t count = 0;
-        struct ProcessStats {
-            std::string comm = "";
-            uint64_t count = 0;
-        };
-        std::vector<ProcessStats> topNProcesses = {};
-    };
-    std::vector<UidStats> topNIoBlockedUids = {};
-    // Total # of tasks owned by each UID in |topNIoBlockedUids|.
-    std::vector<uint64_t> topNIoBlockedUidsTotalTaskCnt = {};
-    std::vector<UidStats> topNMajorFaultUids = {};
-    uint64_t totalMajorFaults = 0;
-    // Percentage of increase/decrease in the major page faults since last collection.
-    double majorFaultsPercentChange = 0.0;
-};
-
-std::string toString(const ProcessIoPerfData& data);
-
-struct IoPerfRecord {
-    time_t time;  // Collection time.
-    UidIoPerfData uidIoPerfData;
-    SystemIoPerfData systemIoPerfData;
-    ProcessIoPerfData processIoPerfData;
-};
-
-std::string toString(const IoPerfRecord& record);
-
-struct CollectionInfo {
-    size_t maxCacheSize = 0;            // Maximum cache size for the collection.
-    std::vector<IoPerfRecord> records;  // Cache of collected performance records.
-};
-
-std::string toString(const CollectionInfo& collectionInfo);
+// Number of periodic collection records to cache in memory.
+constexpr int32_t kDefaultPeriodicCollectionBufferSize = 180;
+constexpr const char kEmptyCollectionMessage[] = "No collection recorded\n";
 
 // Forward declaration for testing use only.
 namespace internal {
@@ -115,13 +50,84 @@
 
 }  // namespace internal
 
+// Below structs should be used only by the implementation and unit tests.
+/**
+ * Struct to represent user package performance stats.
+ */
+struct UserPackageStats {
+    struct IoStats {
+        int64_t bytes[UID_STATES] = {0};
+        int64_t fsync[UID_STATES] = {0};
+
+        int64_t totalBytes() const {
+            return std::numeric_limits<int64_t>::max() - bytes[UidState::FOREGROUND] >
+                            bytes[UidState::BACKGROUND]
+                    ? bytes[UidState::FOREGROUND] + bytes[UidState::BACKGROUND]
+                    : std::numeric_limits<int64_t>::max();
+        }
+    };
+    struct ProcStats {
+        uint64_t count = 0;
+        struct ProcessCount {
+            std::string comm = "";
+            uint64_t count = 0;
+        };
+        std::vector<ProcessCount> topNProcesses = {};
+    };
+    uid_t uid = 0;
+    std::string genericPackageName = "";
+    std::variant<std::monostate, IoStats, ProcStats> stats;
+    std::string toString(MetricType metricsType, const int64_t totalIoStats[][UID_STATES]) const;
+    std::string toString(int64_t count) const;
+};
+
+/**
+ * User package summary performance stats collected from the `/proc/uid_io/stats`,
+ * `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat`, and /proc/[pid]/status` files.
+ */
+struct UserPackageSummaryStats {
+    std::vector<UserPackageStats> topNIoReads = {};
+    std::vector<UserPackageStats> topNIoWrites = {};
+    std::vector<UserPackageStats> topNIoBlocked = {};
+    std::vector<UserPackageStats> topNMajorFaults = {};
+    int64_t totalIoStats[METRIC_TYPES][UID_STATES] = {{0}};
+    std::unordered_map<uid_t, uint64_t> taskCountByUid = {};
+    uint64_t totalMajorFaults = 0;
+    // Percentage of increase/decrease in the major page faults since last collection.
+    double majorFaultsPercentChange = 0.0;
+    std::string toString() const;
+};
+
+// System performance stats collected from the `/proc/stats` file.
+struct SystemSummaryStats {
+    uint64_t cpuIoWaitTime = 0;
+    uint64_t totalCpuTime = 0;
+    uint32_t ioBlockedProcessCount = 0;
+    uint32_t totalProcessCount = 0;
+    std::string toString() const;
+};
+
+// Performance record collected during a sampling/collection period.
+struct PerfStatsRecord {
+    time_t time;  // Collection time.
+    SystemSummaryStats systemSummaryStats;
+    UserPackageSummaryStats userPackageSummaryStats;
+    std::string toString() const;
+};
+
+// Group of performance records collected for a collection event.
+struct CollectionInfo {
+    size_t maxCacheSize = 0;               // Maximum cache size for the collection.
+    std::vector<PerfStatsRecord> records;  // Cache of collected performance records.
+    std::string toString() const;
+};
+
 // IoPerfCollection implements the I/O performance data collection module.
 class IoPerfCollection : public IDataProcessorInterface {
 public:
     IoPerfCollection() :
           mTopNStatsPerCategory(0),
           mTopNStatsPerSubcategory(0),
-          mPackageInfoResolver(PackageInfoResolver::getInstance()),
           mBoottimeCollection({}),
           mPeriodicCollection({}),
           mCustomCollection({}),
@@ -129,36 +135,35 @@
 
     ~IoPerfCollection() { terminate(); }
 
-    std::string name() { return "IoPerfCollection"; }
+    std::string name() const override { return "IoPerfCollection"; }
 
     // Implements IDataProcessorInterface.
-    android::base::Result<void> onBoottimeCollection(time_t time,
-                                                     const android::wp<UidIoStats>& uidIoStats,
-                                                     const android::wp<ProcStat>& procStat,
-                                                     const android::wp<ProcPidStat>& procPidStat);
+    android::base::Result<void> onBoottimeCollection(
+            time_t time, const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::wp<ProcStat>& procStat) override;
 
-    android::base::Result<void> onPeriodicCollection(time_t time, SystemState systemState,
-                                                     const android::wp<UidIoStats>& uidIoStats,
-                                                     const android::wp<ProcStat>& procStat,
-                                                     const android::wp<ProcPidStat>& procPidStat);
+    android::base::Result<void> onPeriodicCollection(
+            time_t time, SystemState systemState,
+            const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::wp<ProcStat>& procStat) override;
 
     android::base::Result<void> onCustomCollection(
             time_t time, SystemState systemState,
             const std::unordered_set<std::string>& filterPackages,
-            const android::wp<UidIoStats>& uidIoStats, const android::wp<ProcStat>& procStat,
-            const android::wp<ProcPidStat>& procPidStat);
+            const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::wp<ProcStat>& procStat) override;
 
     android::base::Result<void> onPeriodicMonitor(
             [[maybe_unused]] time_t time,
             [[maybe_unused]] const android::wp<IProcDiskStatsInterface>& procDiskStats,
-            [[maybe_unused]] const std::function<void()>& alertHandler) {
+            [[maybe_unused]] const std::function<void()>& alertHandler) override {
         // No monitoring done here as this DataProcessor only collects I/O performance records.
         return {};
     }
 
-    android::base::Result<void> onDump(int fd);
+    android::base::Result<void> onDump(int fd) const override;
 
-    android::base::Result<void> onCustomCollectionDump(int fd);
+    android::base::Result<void> onCustomCollectionDump(int fd) override;
 
 protected:
     android::base::Result<void> init();
@@ -168,27 +173,19 @@
 
 private:
     // Processes the collected data.
-    android::base::Result<void> processLocked(time_t time,
-                                              const std::unordered_set<std::string>& filterPackages,
-                                              const android::wp<UidIoStats>& uidIoStats,
-                                              const android::wp<ProcStat>& procStat,
-                                              const android::wp<ProcPidStat>& procPidStat,
-                                              CollectionInfo* collectionInfo);
+    android::base::Result<void> processLocked(
+            time_t time, const std::unordered_set<std::string>& filterPackages,
+            const android::sp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::sp<ProcStat>& procStat, CollectionInfo* collectionInfo);
 
-    // Processes performance data from the `/proc/uid_io/stats` file.
-    void processUidIoPerfData(const std::unordered_set<std::string>& filterPackages,
-                              const android::wp<UidIoStats>& uidIoStats,
-                              UidIoPerfData* uidIoPerfData) const;
+    // Processes per-UID performance data.
+    void processUidStatsLocked(const std::unordered_set<std::string>& filterPackages,
+                               const android::sp<UidStatsCollectorInterface>& uidStatsCollector,
+                               UserPackageSummaryStats* userPackageSummaryStats);
 
-    // Processes performance data from the `/proc/stats` file.
-    void processSystemIoPerfData(const android::wp<ProcStat>& procStat,
-                                 SystemIoPerfData* systemIoPerfData) const;
-
-    // Processes performance data from the `/proc/[pid]/stat` and `/proc/[pid]/task/[tid]/stat`
-    // files.
-    void processProcessIoPerfDataLocked(const std::unordered_set<std::string>& filterPackages,
-                                        const android::wp<ProcPidStat>& procPidStat,
-                                        ProcessIoPerfData* processIoPerfData);
+    // Processes system performance data from the `/proc/stats` file.
+    void processProcStatLocked(const android::sp<ProcStat>& procStat,
+                               SystemSummaryStats* systemSummaryStats) const;
 
     // Top N per-UID stats per category.
     int mTopNStatsPerCategory;
@@ -196,36 +193,34 @@
     // Top N per-process stats per subcategory.
     int mTopNStatsPerSubcategory;
 
-    // Local IPackageInfoResolver instance. Useful to mock in tests.
-    sp<IPackageInfoResolver> mPackageInfoResolver;
-
     // Makes sure only one collection is running at any given time.
-    Mutex mMutex;
+    mutable Mutex mMutex;
 
     // Info for the boot-time collection event. The cache is persisted until system shutdown/reboot.
     CollectionInfo mBoottimeCollection GUARDED_BY(mMutex);
 
-    // Info for the periodic collection event. The cache size is limited by
-    // |ro.carwatchdog.periodic_collection_buffer_size|.
+    /**
+     * Info for the periodic collection event. The cache size is limited by
+     * |ro.carwatchdog.periodic_collection_buffer_size|.
+     */
     CollectionInfo mPeriodicCollection GUARDED_BY(mMutex);
 
-    // Info for the custom collection event. The info is cleared at the end of every custom
-    // collection.
+    /**
+     * Info for the custom collection event. The info is cleared at the end of every custom
+     * collection.
+     */
     CollectionInfo mCustomCollection GUARDED_BY(mMutex);
 
-    // Major faults delta from last collection. Useful when calculating the percentage change in
-    // major faults since last collection.
+    /**
+     * Major faults delta from last collection. Useful when calculating the percentage change in
+     * major faults since last collection.
+     */
     uint64_t mLastMajorFaults GUARDED_BY(mMutex);
 
     friend class WatchdogPerfService;
 
     // For unit tests.
     friend class internal::IoPerfCollectionPeer;
-    FRIEND_TEST(IoPerfCollectionTest, TestUidIoStatsGreaterThanTopNStatsLimit);
-    FRIEND_TEST(IoPerfCollectionTest, TestUidIOStatsLessThanTopNStatsLimit);
-    FRIEND_TEST(IoPerfCollectionTest, TestProcessSystemIoPerfData);
-    FRIEND_TEST(IoPerfCollectionTest, TestProcPidContentsGreaterThanTopNStatsLimit);
-    FRIEND_TEST(IoPerfCollectionTest, TestProcPidContentsLessThanTopNStatsLimit);
 };
 
 }  // namespace watchdog
diff --git a/cpp/watchdog/server/src/PackageInfoResolver.cpp b/cpp/watchdog/server/src/PackageInfoResolver.cpp
index 2b30600..84d7c67 100644
--- a/cpp/watchdog/server/src/PackageInfoResolver.cpp
+++ b/cpp/watchdog/server/src/PackageInfoResolver.cpp
@@ -179,9 +179,23 @@
         if (id.name.empty()) {
             continue;
         }
-        if (const auto it = mPackagesToAppCategories.find(id.name);
-            packageInfo.uidType == UidType::APPLICATION && it != mPackagesToAppCategories.end()) {
-            packageInfo.appCategoryType = it->second;
+        if (packageInfo.uidType == UidType::APPLICATION) {
+            if (const auto it = mPackagesToAppCategories.find(id.name);
+                it != mPackagesToAppCategories.end()) {
+                packageInfo.appCategoryType = it->second;
+            } else if (!packageInfo.sharedUidPackages.empty()) {
+                /* The recommendation for the OEMs is to define the application category mapping
+                 * by the shared package names. However, this a fallback to catch if any mapping is
+                 * defined by the individual package name.
+                 */
+                for (const auto& packageName : packageInfo.sharedUidPackages) {
+                    if (const auto it = mPackagesToAppCategories.find(packageName);
+                        it != mPackagesToAppCategories.end()) {
+                        packageInfo.appCategoryType = it->second;
+                        break;
+                    }
+                }
+            }
         }
         mUidToPackageInfoMapping[id.uid] = packageInfo;
     }
diff --git a/cpp/watchdog/server/src/ProcPidStat.cpp b/cpp/watchdog/server/src/ProcPidStat.cpp
deleted file mode 100644
index 8bf9cf0..0000000
--- a/cpp/watchdog/server/src/ProcPidStat.cpp
+++ /dev/null
@@ -1,348 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#define LOG_TAG "carwatchdogd"
-
-#include "ProcPidStat.h"
-
-#include <android-base/file.h>
-#include <android-base/parseint.h>
-#include <android-base/strings.h>
-#include <dirent.h>
-#include <log/log.h>
-
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::base::EndsWith;
-using ::android::base::Error;
-using ::android::base::ParseInt;
-using ::android::base::ParseUint;
-using ::android::base::ReadFileToString;
-using ::android::base::Result;
-using ::android::base::Split;
-using ::android::base::Trim;
-
-namespace {
-
-enum ReadError {
-    ERR_INVALID_FILE = 0,
-    ERR_FILE_OPEN_READ = 1,
-    NUM_ERRORS = 2,
-};
-
-// /proc/PID/stat or /proc/PID/task/TID/stat format:
-// <pid> <comm> <state> <ppid> <pgrp ID> <session ID> <tty_nr> <tpgid> <flags> <minor faults>
-// <children minor faults> <major faults> <children major faults> <user mode time>
-// <system mode time> <children user mode time> <children kernel mode time> <priority> <nice value>
-// <num threads> <start time since boot> <virtual memory size> <resident set size> <rss soft limit>
-// <start code addr> <end code addr> <start stack addr> <ESP value> <EIP> <bitmap of pending sigs>
-// <bitmap of blocked sigs> <bitmap of ignored sigs> <waiting channel> <num pages swapped>
-// <cumulative pages swapped> <exit signal> <processor #> <real-time prio> <agg block I/O delays>
-// <guest time> <children guest time> <start data addr> <end data addr> <start break addr>
-// <cmd line args start addr> <amd line args end addr> <env start addr> <env end addr> <exit code>
-// Example line: 1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0 ...etc...
-bool parsePidStatLine(const std::string& line, PidStat* pidStat) {
-    std::vector<std::string> fields = Split(line, " ");
-
-    // Note: Regex parsing for the below logic increased the time taken to run the
-    // ProcPidStatTest#TestProcPidStatContentsFromDevice from 151.7ms to 1.3 seconds.
-
-    // Comm string is enclosed with ( ) brackets and may contain space(s). Thus calculate the
-    // commEndOffset based on the field that contains the closing bracket.
-    size_t commEndOffset = 0;
-    for (size_t i = 1; i < fields.size(); ++i) {
-        pidStat->comm += fields[i];
-        if (EndsWith(fields[i], ")")) {
-            commEndOffset = i - 1;
-            break;
-        }
-        pidStat->comm += " ";
-    }
-
-    if (pidStat->comm.front() != '(' || pidStat->comm.back() != ')') {
-        ALOGW("Comm string `%s` not enclosed in brackets", pidStat->comm.c_str());
-        return false;
-    }
-    pidStat->comm.erase(pidStat->comm.begin());
-    pidStat->comm.erase(pidStat->comm.end() - 1);
-
-    // The required data is in the first 22 + |commEndOffset| fields so make sure there are at least
-    // these many fields in the file.
-    if (fields.size() < 22 + commEndOffset || !ParseInt(fields[0], &pidStat->pid) ||
-        !ParseInt(fields[3 + commEndOffset], &pidStat->ppid) ||
-        !ParseUint(fields[11 + commEndOffset], &pidStat->majorFaults) ||
-        !ParseUint(fields[19 + commEndOffset], &pidStat->numThreads) ||
-        !ParseUint(fields[21 + commEndOffset], &pidStat->startTime)) {
-        ALOGW("Invalid proc pid stat contents: \"%s\"", line.c_str());
-        return false;
-    }
-    pidStat->state = fields[2 + commEndOffset];
-    return true;
-}
-
-Result<void> readPidStatFile(const std::string& path, PidStat* pidStat) {
-    std::string buffer;
-    if (!ReadFileToString(path, &buffer)) {
-        return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
-    }
-    std::vector<std::string> lines = Split(std::move(buffer), "\n");
-    if (lines.size() != 1 && (lines.size() != 2 || !lines[1].empty())) {
-        return Error(ERR_INVALID_FILE) << path << " contains " << lines.size() << " lines != 1";
-    }
-    if (!parsePidStatLine(std::move(lines[0]), pidStat)) {
-        return Error(ERR_INVALID_FILE) << "Failed to parse the contents of " << path;
-    }
-    return {};
-}
-
-Result<std::unordered_map<std::string, std::string>> readKeyValueFile(
-        const std::string& path, const std::string& delimiter) {
-    std::string buffer;
-    if (!ReadFileToString(path, &buffer)) {
-        return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
-    }
-    std::unordered_map<std::string, std::string> contents;
-    std::vector<std::string> lines = Split(std::move(buffer), "\n");
-    for (size_t i = 0; i < lines.size(); ++i) {
-        if (lines[i].empty()) {
-            continue;
-        }
-        std::vector<std::string> elements = Split(lines[i], delimiter);
-        if (elements.size() < 2) {
-            return Error(ERR_INVALID_FILE)
-                    << "Line \"" << lines[i] << "\" doesn't contain the delimiter \"" << delimiter
-                    << "\" in file " << path;
-        }
-        std::string key = elements[0];
-        std::string value = Trim(lines[i].substr(key.length() + delimiter.length()));
-        if (contents.find(key) != contents.end()) {
-            return Error(ERR_INVALID_FILE)
-                    << "Duplicate " << key << " line: \"" << lines[i] << "\" in file " << path;
-        }
-        contents[key] = value;
-    }
-    return contents;
-}
-
-// /proc/PID/status file format(*):
-// Tgid:    <Thread group ID of the process>
-// Uid:     <Read UID>   <Effective UID>   <Saved set UID>   <Filesystem UID>
-// VmPeak:  <Peak virtual memory size> kB
-// VmSize:  <Virtual memory size> kB
-// VmHWM:   <Peak resident set size> kB
-// VmRSS:   <Resident set size> kB
-//
-// (*) - Included only the fields that are parsed from the file.
-Result<void> readPidStatusFile(const std::string& path, ProcessStats* processStats) {
-    auto ret = readKeyValueFile(path, ":\t");
-    if (!ret.ok()) {
-        return Error(ret.error().code()) << ret.error();
-    }
-    auto contents = ret.value();
-    if (contents.empty()) {
-        return Error(ERR_INVALID_FILE) << "Empty file " << path;
-    }
-    if (contents.find("Uid") == contents.end() ||
-        !ParseInt(Split(contents["Uid"], "\t")[0], &processStats->uid)) {
-        return Error(ERR_INVALID_FILE) << "Failed to read 'UIDs' from file " << path;
-    }
-    if (contents.find("Tgid") == contents.end() ||
-        !ParseInt(contents["Tgid"], &processStats->tgid)) {
-        return Error(ERR_INVALID_FILE) << "Failed to read 'Tgid' from file " << path;
-    }
-    // Below Vm* fields may not be present for some processes so don't fail when they are missing.
-    if (contents.find("VmPeak") != contents.end() &&
-        !ParseUint(Split(contents["VmPeak"], " ")[0], &processStats->vmPeakKb)) {
-        return Error(ERR_INVALID_FILE) << "Failed to parse 'VmPeak' from file " << path;
-    }
-    if (contents.find("VmSize") != contents.end() &&
-        !ParseUint(Split(contents["VmSize"], " ")[0], &processStats->vmSizeKb)) {
-        return Error(ERR_INVALID_FILE) << "Failed to parse 'VmSize' from file " << path;
-    }
-    if (contents.find("VmHWM") != contents.end() &&
-        !ParseUint(Split(contents["VmHWM"], " ")[0], &processStats->vmHwmKb)) {
-        return Error(ERR_INVALID_FILE) << "Failed to parse 'VmHWM' from file " << path;
-    }
-    if (contents.find("VmRSS") != contents.end() &&
-        !ParseUint(Split(contents["VmRSS"], " ")[0], &processStats->vmRssKb)) {
-        return Error(ERR_INVALID_FILE) << "Failed to parse 'VmRSS' from file " << path;
-    }
-    return {};
-}
-
-}  // namespace
-
-Result<void> ProcPidStat::collect() {
-    if (!mEnabled) {
-        return Error() << "Can not access PID stat files under " << kProcDirPath;
-    }
-
-    Mutex::Autolock lock(mMutex);
-    const auto& processStats = getProcessStatsLocked();
-    if (!processStats.ok()) {
-        return Error() << processStats.error();
-    }
-
-    mDeltaProcessStats.clear();
-    for (const auto& it : *processStats) {
-        const ProcessStats& curStats = it.second;
-        const auto& cachedIt = mLatestProcessStats.find(it.first);
-        if (cachedIt == mLatestProcessStats.end() ||
-            cachedIt->second.process.startTime != curStats.process.startTime) {
-            // New/reused PID so don't calculate the delta.
-            mDeltaProcessStats.emplace_back(curStats);
-            continue;
-        }
-
-        ProcessStats deltaStats = curStats;
-        const ProcessStats& cachedStats = cachedIt->second;
-        deltaStats.process.majorFaults -= cachedStats.process.majorFaults;
-        for (auto& deltaThread : deltaStats.threads) {
-            const auto& cachedThread = cachedStats.threads.find(deltaThread.first);
-            if (cachedThread == cachedStats.threads.end() ||
-                cachedThread->second.startTime != deltaThread.second.startTime) {
-                // New TID or TID reused by the same PID so don't calculate the delta.
-                continue;
-            }
-            deltaThread.second.majorFaults -= cachedThread->second.majorFaults;
-        }
-        mDeltaProcessStats.emplace_back(deltaStats);
-    }
-    mLatestProcessStats = *processStats;
-    return {};
-}
-
-Result<std::unordered_map<pid_t, ProcessStats>> ProcPidStat::getProcessStatsLocked() const {
-    std::unordered_map<pid_t, ProcessStats> processStats;
-    auto procDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(mPath.c_str()), closedir);
-    if (!procDirp) {
-        return Error() << "Failed to open " << mPath << " directory";
-    }
-    dirent* pidDir = nullptr;
-    while ((pidDir = readdir(procDirp.get())) != nullptr) {
-        // 1. Read top-level pid stats.
-        pid_t pid = 0;
-        if (pidDir->d_type != DT_DIR || !ParseInt(pidDir->d_name, &pid)) {
-            continue;
-        }
-        ProcessStats curStats;
-        std::string path = StringPrintf((mPath + kStatFileFormat).c_str(), pid);
-        auto ret = readPidStatFile(path, &curStats.process);
-        if (!ret.ok()) {
-            // PID may disappear between scanning the directory and parsing the stat file.
-            // Thus treat ERR_FILE_OPEN_READ errors as soft errors.
-            if (ret.error().code() != ERR_FILE_OPEN_READ) {
-                return Error() << "Failed to read top-level per-process stat file: "
-                               << ret.error().message().c_str();
-            }
-            ALOGW("Failed to read top-level per-process stat file %s: %s", path.c_str(),
-                  ret.error().message().c_str());
-            continue;
-        }
-
-        // 2. Read aggregated process status.
-        path = StringPrintf((mPath + kStatusFileFormat).c_str(), curStats.process.pid);
-        ret = readPidStatusFile(path, &curStats);
-        if (!ret.ok()) {
-            if (ret.error().code() != ERR_FILE_OPEN_READ) {
-                return Error() << "Failed to read pid status for pid " << curStats.process.pid
-                               << ": " << ret.error().message().c_str();
-            }
-            ALOGW("Failed to read pid status for pid %" PRIu32 ": %s", curStats.process.pid,
-                  ret.error().message().c_str());
-        }
-
-        // 3. When failed to read tgid or uid, copy these from the previous collection.
-        if (curStats.tgid == -1 || curStats.uid == -1) {
-            const auto& it = mLatestProcessStats.find(curStats.process.pid);
-            if (it != mLatestProcessStats.end() &&
-                it->second.process.startTime == curStats.process.startTime) {
-                curStats.tgid = it->second.tgid;
-                curStats.uid = it->second.uid;
-            }
-        }
-
-        if (curStats.tgid != -1 && curStats.tgid != curStats.process.pid) {
-            ALOGW("Skipping non-process (i.e., Tgid != PID) entry for PID %" PRIu32,
-                  curStats.process.pid);
-            continue;
-        }
-
-        // 3. Fetch per-thread stats.
-        std::string taskDir = StringPrintf((mPath + kTaskDirFormat).c_str(), pid);
-        auto taskDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(taskDir.c_str()), closedir);
-        if (!taskDirp) {
-            // Treat this as a soft error so at least the process stats will be collected.
-            ALOGW("Failed to open %s directory", taskDir.c_str());
-        }
-        dirent* tidDir = nullptr;
-        bool didReadMainThread = false;
-        while (taskDirp != nullptr && (tidDir = readdir(taskDirp.get())) != nullptr) {
-            pid_t tid = 0;
-            if (tidDir->d_type != DT_DIR || !ParseInt(tidDir->d_name, &tid)) {
-                continue;
-            }
-            if (processStats.find(tid) != processStats.end()) {
-                return Error() << "Process stats already exists for TID " << tid
-                               << ". Stats will be double counted";
-            }
-
-            PidStat curThreadStat = {};
-            path = StringPrintf((taskDir + kStatFileFormat).c_str(), tid);
-            const auto& ret = readPidStatFile(path, &curThreadStat);
-            if (!ret.ok()) {
-                if (ret.error().code() != ERR_FILE_OPEN_READ) {
-                    return Error() << "Failed to read per-thread stat file: "
-                                   << ret.error().message().c_str();
-                }
-                // Maybe the thread terminated before reading the file so skip this thread and
-                // continue with scanning the next thread's stat.
-                ALOGW("Failed to read per-thread stat file %s: %s", path.c_str(),
-                      ret.error().message().c_str());
-                continue;
-            }
-            if (curThreadStat.pid == curStats.process.pid) {
-                didReadMainThread = true;
-            }
-            curStats.threads[curThreadStat.pid] = curThreadStat;
-        }
-        if (!didReadMainThread) {
-            // In the event of failure to read main-thread info (mostly because the process
-            // terminated during scanning/parsing), fill out the stat that are common between main
-            // thread and the process.
-            curStats.threads[curStats.process.pid] = PidStat{
-                    .pid = curStats.process.pid,
-                    .comm = curStats.process.comm,
-                    .state = curStats.process.state,
-                    .ppid = curStats.process.ppid,
-                    .numThreads = curStats.process.numThreads,
-                    .startTime = curStats.process.startTime,
-            };
-        }
-        processStats[curStats.process.pid] = curStats;
-    }
-    return processStats;
-}
-
-}  // namespace watchdog
-}  // namespace automotive
-}  // namespace android
diff --git a/cpp/watchdog/server/src/ProcPidStat.h b/cpp/watchdog/server/src/ProcPidStat.h
deleted file mode 100644
index de1988a..0000000
--- a/cpp/watchdog/server/src/ProcPidStat.h
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#ifndef CPP_WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
-#define CPP_WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
-
-#include <android-base/result.h>
-#include <android-base/stringprintf.h>
-#include <gtest/gtest_prod.h>
-#include <inttypes.h>
-#include <stdint.h>
-#include <utils/Mutex.h>
-#include <utils/RefBase.h>
-
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::base::StringPrintf;
-
-#define PID_FOR_INIT 1
-
-constexpr const char* kProcDirPath = "/proc";
-constexpr const char* kStatFileFormat = "/%" PRIu32 "/stat";
-constexpr const char* kTaskDirFormat = "/%" PRIu32 "/task";
-constexpr const char* kStatusFileFormat = "/%" PRIu32 "/status";
-
-struct PidStat {
-    pid_t pid = 0;
-    std::string comm = "";
-    std::string state = "";
-    pid_t ppid = 0;
-    uint64_t majorFaults = 0;
-    uint32_t numThreads = 0;
-    uint64_t startTime = 0;  // Useful when identifying PID/TID reuse
-};
-
-struct ProcessStats {
-    int64_t tgid = -1;                              // -1 indicates a failure to read this value
-    int64_t uid = -1;                               // -1 indicates a failure to read this value
-    uint64_t vmPeakKb = 0;
-    uint64_t vmSizeKb = 0;
-    uint64_t vmHwmKb = 0;
-    uint64_t vmRssKb = 0;
-    PidStat process = {};                           // Aggregated stats across all the threads
-    std::unordered_map<pid_t, PidStat> threads;     // Per-thread stat including the main thread
-};
-
-// Collector/parser for `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat` and /proc/[pid]/status`
-// files.
-class ProcPidStat : public RefBase {
-public:
-    explicit ProcPidStat(const std::string& path = kProcDirPath) :
-          mLatestProcessStats({}),
-          mPath(path) {
-        std::string pidStatPath = StringPrintf((mPath + kStatFileFormat).c_str(), PID_FOR_INIT);
-        std::string tidStatPath = StringPrintf((mPath + kTaskDirFormat + kStatFileFormat).c_str(),
-                                               PID_FOR_INIT, PID_FOR_INIT);
-        std::string pidStatusPath = StringPrintf((mPath + kStatusFileFormat).c_str(), PID_FOR_INIT);
-
-        mEnabled = !access(pidStatPath.c_str(), R_OK) && !access(tidStatPath.c_str(), R_OK) &&
-                !access(pidStatusPath.c_str(), R_OK);
-    }
-
-    virtual ~ProcPidStat() {}
-
-    // Collects per-process stats.
-    virtual android::base::Result<void> collect();
-
-    // Returns the latest per-process stats collected.
-    virtual const std::unordered_map<pid_t, ProcessStats> latestStats() const {
-        Mutex::Autolock lock(mMutex);
-        return mLatestProcessStats;
-    }
-
-    // Returns the delta of per-process stats since the last before collection.
-    virtual const std::vector<ProcessStats> deltaStats() const {
-        Mutex::Autolock lock(mMutex);
-        return mDeltaProcessStats;
-    }
-
-    // Called by WatchdogPerfService and tests.
-    virtual bool enabled() { return mEnabled; }
-
-    virtual std::string dirPath() { return mPath; }
-
-private:
-    // Reads the contents of the below files:
-    // 1. Pid stat file at |mPath| + |kStatFileFormat|
-    // 2. Aggregated per-process status at |mPath| + |kStatusFileFormat|
-    // 3. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
-    android::base::Result<std::unordered_map<pid_t, ProcessStats>> getProcessStatsLocked() const;
-
-    // Makes sure only one collection is running at any given time.
-    mutable Mutex mMutex;
-
-    // Latest dump of per-process stats. Useful for calculating the delta and identifying PID/TID
-    // reuse.
-    std::unordered_map<pid_t, ProcessStats> mLatestProcessStats GUARDED_BY(mMutex);
-
-    // Latest delta of per-process stats.
-    std::vector<ProcessStats> mDeltaProcessStats GUARDED_BY(mMutex);
-
-    // True if the below files are accessible:
-    // 1. Pid stat file at |mPath| + |kTaskStatFileFormat|
-    // 2. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
-    // 3. Pid status file at |mPath| + |kStatusFileFormat|
-    // Otherwise, set to false.
-    bool mEnabled;
-
-    // Proc directory path. Default value is |kProcDirPath|.
-    // Updated by tests to point to a different location when needed.
-    std::string mPath;
-
-    FRIEND_TEST(IoPerfCollectionTest, TestValidProcPidContents);
-    FRIEND_TEST(ProcPidStatTest, TestValidStatFiles);
-    FRIEND_TEST(ProcPidStatTest, TestHandlesProcessTerminationBetweenScanningAndParsing);
-    FRIEND_TEST(ProcPidStatTest, TestHandlesPidTidReuse);
-};
-
-}  // namespace watchdog
-}  // namespace automotive
-}  // namespace android
-
-#endif  //  CPP_WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
diff --git a/cpp/watchdog/server/src/ProcStat.cpp b/cpp/watchdog/server/src/ProcStat.cpp
index 0a78dcd..7c5337b 100644
--- a/cpp/watchdog/server/src/ProcStat.cpp
+++ b/cpp/watchdog/server/src/ProcStat.cpp
@@ -42,8 +42,9 @@
 bool parseCpuStats(const std::string& data, CpuStats* cpuStats) {
     std::vector<std::string> fields = Split(data, " ");
     if (fields.size() == 12 && fields[1].empty()) {
-        // The first cpu line will have an extra space after the first word. This will generate an
-        // empty element when the line is split on " ". Erase the extra element.
+        /* The first cpu line will have an extra space after the first word. This will generate an
+         * empty element when the line is split on " ". Erase the extra element.
+         */
         fields.erase(fields.begin() + 1);
     }
     if (fields.size() != 11 || fields[0] != "cpu" || !ParseUint(fields[1], &cpuStats->userTime) ||
@@ -115,7 +116,7 @@
                 if (didReadProcsRunning) {
                     return Error() << "Duplicate `procs_running .*` line in " << kPath;
                 }
-                if (!parseProcsCount(std::move(lines[i]), &info.runnableProcessesCnt)) {
+                if (!parseProcsCount(std::move(lines[i]), &info.runnableProcessCount)) {
                     return Error() << "Failed to parse `procs_running .*` line in " << kPath;
                 }
                 didReadProcsRunning = true;
@@ -124,7 +125,7 @@
                 if (didReadProcsBlocked) {
                     return Error() << "Duplicate `procs_blocked .*` line in " << kPath;
                 }
-                if (!parseProcsCount(std::move(lines[i]), &info.ioBlockedProcessesCnt)) {
+                if (!parseProcsCount(std::move(lines[i]), &info.ioBlockedProcessCount)) {
                     return Error() << "Failed to parse `procs_blocked .*` line in " << kPath;
                 }
                 didReadProcsBlocked = true;
diff --git a/cpp/watchdog/server/src/ProcStat.h b/cpp/watchdog/server/src/ProcStat.h
index 2e660a1..3bb03b4 100644
--- a/cpp/watchdog/server/src/ProcStat.h
+++ b/cpp/watchdog/server/src/ProcStat.h
@@ -18,10 +18,11 @@
 #define CPP_WATCHDOG_SERVER_SRC_PROCSTAT_H_
 
 #include <android-base/result.h>
-#include <stdint.h>
 #include <utils/Mutex.h>
 #include <utils/RefBase.h>
 
+#include <stdint.h>
+
 namespace android {
 namespace automotive {
 namespace watchdog {
@@ -57,28 +58,31 @@
 
 class ProcStatInfo {
 public:
-    ProcStatInfo() : cpuStats({}), runnableProcessesCnt(0), ioBlockedProcessesCnt(0) {}
+    ProcStatInfo() : cpuStats({}), runnableProcessCount(0), ioBlockedProcessCount(0) {}
     ProcStatInfo(CpuStats stats, uint32_t runnableCnt, uint32_t ioBlockedCnt) :
-          cpuStats(stats), runnableProcessesCnt(runnableCnt), ioBlockedProcessesCnt(ioBlockedCnt) {}
+          cpuStats(stats),
+          runnableProcessCount(runnableCnt),
+          ioBlockedProcessCount(ioBlockedCnt) {}
     CpuStats cpuStats;
-    uint32_t runnableProcessesCnt;
-    uint32_t ioBlockedProcessesCnt;
+    uint32_t runnableProcessCount;
+    uint32_t ioBlockedProcessCount;
 
     uint64_t totalCpuTime() const {
         return cpuStats.userTime + cpuStats.niceTime + cpuStats.sysTime + cpuStats.idleTime +
                 cpuStats.ioWaitTime + cpuStats.irqTime + cpuStats.softIrqTime + cpuStats.stealTime +
                 cpuStats.guestTime + cpuStats.guestNiceTime;
     }
-    uint32_t totalProcessesCnt() const { return runnableProcessesCnt + ioBlockedProcessesCnt; }
+    uint32_t totalProcessCount() const { return runnableProcessCount + ioBlockedProcessCount; }
     bool operator==(const ProcStatInfo& info) const {
         return memcmp(&cpuStats, &info.cpuStats, sizeof(cpuStats)) == 0 &&
-                runnableProcessesCnt == info.runnableProcessesCnt &&
-                ioBlockedProcessesCnt == info.ioBlockedProcessesCnt;
+                runnableProcessCount == info.runnableProcessCount &&
+                ioBlockedProcessCount == info.ioBlockedProcessCount;
     }
     ProcStatInfo& operator-=(const ProcStatInfo& rhs) {
         cpuStats -= rhs.cpuStats;
-        // Don't diff *ProcessesCnt as they are real-time values unlike |cpuStats|, which are
-        // aggregated values since system startup.
+        /* Don't diff *ProcessCount as they are real-time values unlike |cpuStats|, which are
+         * aggregated values since system startup.
+         */
         return *this;
     }
 };
@@ -96,8 +100,9 @@
     // Collects proc stat delta since the last collection.
     virtual android::base::Result<void> collect();
 
-    // Returns true when the proc stat file is accessible. Otherwise, returns false.
-    // Called by WatchdogPerfService and tests.
+    /* Returns true when the proc stat file is accessible. Otherwise, returns false.
+     * Called by WatchdogPerfService and tests.
+     */
     virtual bool enabled() { return kEnabled; }
 
     virtual std::string filePath() { return kProcStatPath; }
diff --git a/cpp/watchdog/server/src/UidIoStats.cpp b/cpp/watchdog/server/src/UidIoStats.cpp
deleted file mode 100644
index e88693a..0000000
--- a/cpp/watchdog/server/src/UidIoStats.cpp
+++ /dev/null
@@ -1,157 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#define LOG_TAG "carwatchdogd"
-
-#include "UidIoStats.h"
-
-#include <android-base/file.h>
-#include <android-base/parseint.h>
-#include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <log/log.h>
-
-#include <inttypes.h>
-
-#include <string>
-#include <unordered_map>
-#include <utility>
-#include <vector>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::base::Error;
-using ::android::base::ParseInt;
-using ::android::base::ParseUint;
-using ::android::base::ReadFileToString;
-using ::android::base::Result;
-using ::android::base::Split;
-using ::android::base::StringPrintf;
-
-namespace {
-
-bool parseUidIoStats(const std::string& data, UidIoUsage* usage) {
-    std::vector<std::string> fields = Split(data, " ");
-    if (fields.size() < 11 || !ParseUint(fields[0], &usage->uid) ||
-        !ParseInt(fields[3], &usage->ios.metrics[READ_BYTES][FOREGROUND]) ||
-        !ParseInt(fields[4], &usage->ios.metrics[WRITE_BYTES][FOREGROUND]) ||
-        !ParseInt(fields[7], &usage->ios.metrics[READ_BYTES][BACKGROUND]) ||
-        !ParseInt(fields[8], &usage->ios.metrics[WRITE_BYTES][BACKGROUND]) ||
-        !ParseInt(fields[9], &usage->ios.metrics[FSYNC_COUNT][FOREGROUND]) ||
-        !ParseInt(fields[10], &usage->ios.metrics[FSYNC_COUNT][BACKGROUND])) {
-        ALOGW("Invalid uid I/O stats: \"%s\"", data.c_str());
-        return false;
-    }
-    return true;
-}
-
-int64_t maybeDiff(int64_t lhs, int64_t rhs) {
-    return lhs > rhs ? lhs - rhs : 0;
-}
-
-}  // namespace
-
-IoUsage& IoUsage::operator-=(const IoUsage& rhs) {
-    metrics[READ_BYTES][FOREGROUND] =
-            maybeDiff(metrics[READ_BYTES][FOREGROUND], rhs.metrics[READ_BYTES][FOREGROUND]);
-    metrics[READ_BYTES][BACKGROUND] =
-            maybeDiff(metrics[READ_BYTES][BACKGROUND], rhs.metrics[READ_BYTES][BACKGROUND]);
-    metrics[WRITE_BYTES][FOREGROUND] =
-            maybeDiff(metrics[WRITE_BYTES][FOREGROUND], rhs.metrics[WRITE_BYTES][FOREGROUND]);
-    metrics[WRITE_BYTES][BACKGROUND] =
-            maybeDiff(metrics[WRITE_BYTES][BACKGROUND], rhs.metrics[WRITE_BYTES][BACKGROUND]);
-    metrics[FSYNC_COUNT][FOREGROUND] =
-            maybeDiff(metrics[FSYNC_COUNT][FOREGROUND], rhs.metrics[FSYNC_COUNT][FOREGROUND]);
-    metrics[FSYNC_COUNT][BACKGROUND] =
-            maybeDiff(metrics[FSYNC_COUNT][BACKGROUND], rhs.metrics[FSYNC_COUNT][BACKGROUND]);
-    return *this;
-}
-
-bool IoUsage::isZero() const {
-    for (int i = 0; i < METRIC_TYPES; i++) {
-        for (int j = 0; j < UID_STATES; j++) {
-            if (metrics[i][j]) {
-                return false;
-            }
-        }
-    }
-    return true;
-}
-
-std::string IoUsage::toString() const {
-    return StringPrintf("FgRdBytes:%" PRIi64 " BgRdBytes:%" PRIi64 " FgWrBytes:%" PRIi64
-                        " BgWrBytes:%" PRIi64 " FgFsync:%" PRIi64 " BgFsync:%" PRIi64,
-                        metrics[READ_BYTES][FOREGROUND], metrics[READ_BYTES][BACKGROUND],
-                        metrics[WRITE_BYTES][FOREGROUND], metrics[WRITE_BYTES][BACKGROUND],
-                        metrics[FSYNC_COUNT][FOREGROUND], metrics[FSYNC_COUNT][BACKGROUND]);
-}
-
-Result<void> UidIoStats::collect() {
-    if (!kEnabled) {
-        return Error() << "Can not access " << kPath;
-    }
-
-    Mutex::Autolock lock(mMutex);
-    const auto& uidIoUsages = getUidIoUsagesLocked();
-    if (!uidIoUsages.ok() || uidIoUsages->empty()) {
-        return Error() << "Failed to get UID IO stats: " << uidIoUsages.error();
-    }
-
-    mDeltaUidIoUsages.clear();
-    for (const auto& it : *uidIoUsages) {
-        UidIoUsage curUsage = it.second;
-        if (curUsage.ios.isZero()) {
-            continue;
-        }
-        if (mLatestUidIoUsages.find(it.first) != mLatestUidIoUsages.end()) {
-            if (curUsage -= mLatestUidIoUsages[it.first]; curUsage.ios.isZero()) {
-                continue;
-            }
-        }
-        mDeltaUidIoUsages[it.first] = curUsage;
-    }
-    mLatestUidIoUsages = *uidIoUsages;
-    return {};
-}
-
-Result<std::unordered_map<uid_t, UidIoUsage>> UidIoStats::getUidIoUsagesLocked() const {
-    std::string buffer;
-    if (!ReadFileToString(kPath, &buffer)) {
-        return Error() << "ReadFileToString failed for " << kPath;
-    }
-
-    std::vector<std::string> ioStats = Split(std::move(buffer), "\n");
-    std::unordered_map<uid_t, UidIoUsage> uidIoUsages;
-    UidIoUsage usage;
-    for (size_t i = 0; i < ioStats.size(); i++) {
-        if (ioStats[i].empty() || !ioStats[i].compare(0, 4, "task")) {
-            // Skip per-task stats as CONFIG_UID_SYS_STATS_DEBUG is not set in the kernel and
-            // the collected data is aggregated only per-UID.
-            continue;
-        }
-        if (!parseUidIoStats(std::move(ioStats[i]), &usage)) {
-            return Error() << "Failed to parse the contents of " << kPath;
-        }
-        uidIoUsages[usage.uid] = usage;
-    }
-    return uidIoUsages;
-}
-
-}  // namespace watchdog
-}  // namespace automotive
-}  // namespace android
diff --git a/cpp/watchdog/server/src/UidIoStats.h b/cpp/watchdog/server/src/UidIoStats.h
deleted file mode 100644
index b3257f8..0000000
--- a/cpp/watchdog/server/src/UidIoStats.h
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#ifndef CPP_WATCHDOG_SERVER_SRC_UIDIOSTATS_H_
-#define CPP_WATCHDOG_SERVER_SRC_UIDIOSTATS_H_
-
-#include <android-base/result.h>
-#include <android-base/stringprintf.h>
-#include <utils/Mutex.h>
-#include <utils/RefBase.h>
-
-#include <stdint.h>
-
-#include <string>
-#include <unordered_map>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-constexpr const char* kUidIoStatsPath = "/proc/uid_io/stats";
-
-enum UidState {
-    FOREGROUND = 0,
-    BACKGROUND,
-    UID_STATES,
-};
-
-enum MetricType {
-    READ_BYTES = 0,  // bytes read (from storage layer)
-    WRITE_BYTES,     // bytes written (to storage layer)
-    FSYNC_COUNT,     // number of fsync syscalls
-    METRIC_TYPES,
-};
-
-class IoUsage {
-public:
-    IoUsage() : metrics{{0}} {};
-    IoUsage(int64_t fgRdBytes, int64_t bgRdBytes, int64_t fgWrBytes, int64_t bgWrBytes,
-            int64_t fgFsync, int64_t bgFsync) {
-        metrics[READ_BYTES][FOREGROUND] = fgRdBytes;
-        metrics[READ_BYTES][BACKGROUND] = bgRdBytes;
-        metrics[WRITE_BYTES][FOREGROUND] = fgWrBytes;
-        metrics[WRITE_BYTES][BACKGROUND] = bgWrBytes;
-        metrics[FSYNC_COUNT][FOREGROUND] = fgFsync;
-        metrics[FSYNC_COUNT][BACKGROUND] = bgFsync;
-    }
-    IoUsage& operator-=(const IoUsage& rhs);
-    bool operator==(const IoUsage& usage) const {
-        return memcmp(&metrics, &usage.metrics, sizeof(metrics)) == 0;
-    }
-    int64_t sumReadBytes() const {
-        const auto& [fgBytes, bgBytes] =
-                std::tuple(metrics[READ_BYTES][FOREGROUND], metrics[READ_BYTES][BACKGROUND]);
-        return (std::numeric_limits<int64_t>::max() - fgBytes) > bgBytes
-                ? (fgBytes + bgBytes)
-                : std::numeric_limits<int64_t>::max();
-    }
-    int64_t sumWriteBytes() const {
-        const auto& [fgBytes, bgBytes] =
-                std::tuple(metrics[WRITE_BYTES][FOREGROUND], metrics[WRITE_BYTES][BACKGROUND]);
-        return (std::numeric_limits<int64_t>::max() - fgBytes) > bgBytes
-                ? (fgBytes + bgBytes)
-                : std::numeric_limits<int64_t>::max();
-    }
-    bool isZero() const;
-    std::string toString() const;
-    int64_t metrics[METRIC_TYPES][UID_STATES];
-};
-
-struct UidIoUsage {
-    uid_t uid = 0;  // Linux user id.
-    IoUsage ios = {};
-    UidIoUsage& operator-=(const UidIoUsage& rhs) {
-        ios -= rhs.ios;
-        return *this;
-    }
-    bool operator==(const UidIoUsage& rhs) const { return uid == rhs.uid && ios == rhs.ios; }
-    std::string toString() const {
-        return android::base::StringPrintf("Uid: %d, Usage: {%s}", uid, ios.toString().c_str());
-    }
-};
-
-class UidIoStats : public RefBase {
-public:
-    explicit UidIoStats(const std::string& path = kUidIoStatsPath) :
-          kEnabled(!access(path.c_str(), R_OK)), kPath(path) {}
-
-    virtual ~UidIoStats() {}
-
-    // Collects the per-UID I/O usage.
-    virtual android::base::Result<void> collect();
-
-    virtual const std::unordered_map<uid_t, UidIoUsage> latestStats() const {
-        Mutex::Autolock lock(mMutex);
-        return mLatestUidIoUsages;
-    }
-
-    virtual const std::unordered_map<uid_t, UidIoUsage> deltaStats() const {
-        Mutex::Autolock lock(mMutex);
-        return mDeltaUidIoUsages;
-    }
-
-    // Returns true when the uid_io stats file is accessible. Otherwise, returns false.
-    // Called by IoPerfCollection and tests.
-    virtual bool enabled() { return kEnabled; }
-
-    virtual std::string filePath() { return kPath; }
-
-private:
-    // Reads the contents of |kPath|.
-    android::base::Result<std::unordered_map<uid_t, UidIoUsage>> getUidIoUsagesLocked() const;
-
-    // Makes sure only one collection is running at any given time.
-    mutable Mutex mMutex;
-
-    // Latest dump from the file at |kPath|.
-    std::unordered_map<uid_t, UidIoUsage> mLatestUidIoUsages GUARDED_BY(mMutex);
-
-    // Delta of per-UID I/O usage since last before collection.
-    std::unordered_map<uid_t, UidIoUsage> mDeltaUidIoUsages GUARDED_BY(mMutex);
-
-    // True if kPath is accessible.
-    const bool kEnabled;
-
-    // Path to uid_io stats file. Default path is |kUidIoStatsPath|.
-    const std::string kPath;
-};
-
-}  // namespace watchdog
-}  // namespace automotive
-}  // namespace android
-
-#endif  //  CPP_WATCHDOG_SERVER_SRC_UIDIOSTATS_H_
diff --git a/cpp/watchdog/server/src/UidIoStatsCollector.cpp b/cpp/watchdog/server/src/UidIoStatsCollector.cpp
new file mode 100644
index 0000000..9e8bc32
--- /dev/null
+++ b/cpp/watchdog/server/src/UidIoStatsCollector.cpp
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#define LOG_TAG "carwatchdogd"
+
+#include "UidIoStatsCollector.h"
+
+#include <android-base/file.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <log/log.h>
+
+#include <inttypes.h>
+
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::base::Error;
+using ::android::base::ParseInt;
+using ::android::base::ParseUint;
+using ::android::base::ReadFileToString;
+using ::android::base::Result;
+using ::android::base::Split;
+using ::android::base::StringPrintf;
+
+namespace {
+
+bool parseUidIoStats(const std::string& data, UidIoStats* stats, uid_t* uid) {
+    std::vector<std::string> fields = Split(data, " ");
+    if (fields.size() < 11 || !ParseUint(fields[0], uid) ||
+        !ParseInt(fields[3], &stats->metrics[READ_BYTES][FOREGROUND]) ||
+        !ParseInt(fields[4], &stats->metrics[WRITE_BYTES][FOREGROUND]) ||
+        !ParseInt(fields[7], &stats->metrics[READ_BYTES][BACKGROUND]) ||
+        !ParseInt(fields[8], &stats->metrics[WRITE_BYTES][BACKGROUND]) ||
+        !ParseInt(fields[9], &stats->metrics[FSYNC_COUNT][FOREGROUND]) ||
+        !ParseInt(fields[10], &stats->metrics[FSYNC_COUNT][BACKGROUND])) {
+        ALOGW("Invalid uid I/O stats: \"%s\"", data.c_str());
+        return false;
+    }
+    return true;
+}
+
+}  // namespace
+
+UidIoStats& UidIoStats::operator-=(const UidIoStats& rhs) {
+    const auto diff = [](int64_t lhs, int64_t rhs) -> int64_t { return lhs > rhs ? lhs - rhs : 0; };
+    metrics[READ_BYTES][FOREGROUND] =
+            diff(metrics[READ_BYTES][FOREGROUND], rhs.metrics[READ_BYTES][FOREGROUND]);
+    metrics[READ_BYTES][BACKGROUND] =
+            diff(metrics[READ_BYTES][BACKGROUND], rhs.metrics[READ_BYTES][BACKGROUND]);
+    metrics[WRITE_BYTES][FOREGROUND] =
+            diff(metrics[WRITE_BYTES][FOREGROUND], rhs.metrics[WRITE_BYTES][FOREGROUND]);
+    metrics[WRITE_BYTES][BACKGROUND] =
+            diff(metrics[WRITE_BYTES][BACKGROUND], rhs.metrics[WRITE_BYTES][BACKGROUND]);
+    metrics[FSYNC_COUNT][FOREGROUND] =
+            diff(metrics[FSYNC_COUNT][FOREGROUND], rhs.metrics[FSYNC_COUNT][FOREGROUND]);
+    metrics[FSYNC_COUNT][BACKGROUND] =
+            diff(metrics[FSYNC_COUNT][BACKGROUND], rhs.metrics[FSYNC_COUNT][BACKGROUND]);
+    return *this;
+}
+
+bool UidIoStats::isZero() const {
+    for (int i = 0; i < METRIC_TYPES; i++) {
+        for (int j = 0; j < UID_STATES; j++) {
+            if (metrics[i][j]) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+std::string UidIoStats::toString() const {
+    return StringPrintf("FgRdBytes:%" PRIi64 " BgRdBytes:%" PRIi64 " FgWrBytes:%" PRIi64
+                        " BgWrBytes:%" PRIi64 " FgFsync:%" PRIi64 " BgFsync:%" PRIi64,
+                        metrics[READ_BYTES][FOREGROUND], metrics[READ_BYTES][BACKGROUND],
+                        metrics[WRITE_BYTES][FOREGROUND], metrics[WRITE_BYTES][BACKGROUND],
+                        metrics[FSYNC_COUNT][FOREGROUND], metrics[FSYNC_COUNT][BACKGROUND]);
+}
+
+Result<void> UidIoStatsCollector::collect() {
+    if (!kEnabled) {
+        return Error() << "Can not access " << kPath;
+    }
+
+    Mutex::Autolock lock(mMutex);
+    const auto& uidIoStatsByUid = readUidIoStatsLocked();
+    if (!uidIoStatsByUid.ok() || uidIoStatsByUid->empty()) {
+        return Error() << "Failed to get UID IO stats: " << uidIoStatsByUid.error();
+    }
+
+    mDeltaStats.clear();
+    for (const auto& [uid, uidIoStats] : *uidIoStatsByUid) {
+        if (uidIoStats.isZero()) {
+            continue;
+        }
+        UidIoStats deltaStats = uidIoStats;
+        if (const auto it = mLatestStats.find(uid); it != mLatestStats.end()) {
+            if (deltaStats -= it->second; deltaStats.isZero()) {
+                continue;
+            }
+        }
+        mDeltaStats[uid] = deltaStats;
+    }
+    mLatestStats = *uidIoStatsByUid;
+    return {};
+}
+
+Result<std::unordered_map<uid_t, UidIoStats>> UidIoStatsCollector::readUidIoStatsLocked() const {
+    std::string buffer;
+    if (!ReadFileToString(kPath, &buffer)) {
+        return Error() << "ReadFileToString failed for " << kPath;
+    }
+    std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid;
+    std::vector<std::string> lines = Split(std::move(buffer), "\n");
+    for (const auto& line : lines) {
+        if (line.empty() || !line.compare(0, 4, "task")) {
+            /* Skip per-task stats as CONFIG_UID_SYS_STATS_DEBUG is not set in the kernel and
+             * the collected data is aggregated only per-UID.
+             */
+            continue;
+        }
+        uid_t uid;
+        UidIoStats uidIoStats;
+        if (!parseUidIoStats(line, &uidIoStats, &uid)) {
+            return Error() << "Failed to parse the contents of " << kPath;
+        }
+        uidIoStatsByUid[uid] = uidIoStats;
+    }
+    return uidIoStatsByUid;
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/watchdog/server/src/UidIoStatsCollector.h b/cpp/watchdog/server/src/UidIoStatsCollector.h
new file mode 100644
index 0000000..b5f4494
--- /dev/null
+++ b/cpp/watchdog/server/src/UidIoStatsCollector.h
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_SRC_UIDIOSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_SRC_UIDIOSTATSCOLLECTOR_H_
+
+#include <android-base/result.h>
+#include <android-base/stringprintf.h>
+#include <utils/Mutex.h>
+#include <utils/RefBase.h>
+
+#include <stdint.h>
+
+#include <string>
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+constexpr const char* kUidIoStatsPath = "/proc/uid_io/stats";
+
+enum UidState {
+    FOREGROUND = 0,
+    BACKGROUND,
+    UID_STATES,
+};
+
+enum MetricType {
+    READ_BYTES = 0,  // bytes read (from storage layer)
+    WRITE_BYTES,     // bytes written (to storage layer)
+    FSYNC_COUNT,     // number of fsync syscalls
+    METRIC_TYPES,
+};
+
+// Defines the per-UID I/O stats.
+class UidIoStats {
+public:
+    UidIoStats() : metrics{{0}} {};
+    UidIoStats(int64_t fgRdBytes, int64_t bgRdBytes, int64_t fgWrBytes, int64_t bgWrBytes,
+               int64_t fgFsync, int64_t bgFsync) {
+        metrics[READ_BYTES][FOREGROUND] = fgRdBytes;
+        metrics[READ_BYTES][BACKGROUND] = bgRdBytes;
+        metrics[WRITE_BYTES][FOREGROUND] = fgWrBytes;
+        metrics[WRITE_BYTES][BACKGROUND] = bgWrBytes;
+        metrics[FSYNC_COUNT][FOREGROUND] = fgFsync;
+        metrics[FSYNC_COUNT][BACKGROUND] = bgFsync;
+    }
+    UidIoStats& operator-=(const UidIoStats& rhs);
+    bool operator==(const UidIoStats& stats) const {
+        return memcmp(&metrics, &stats.metrics, sizeof(metrics)) == 0;
+    }
+    int64_t sumReadBytes() const {
+        const auto& [fgBytes, bgBytes] =
+                std::tuple(metrics[READ_BYTES][FOREGROUND], metrics[READ_BYTES][BACKGROUND]);
+        return (std::numeric_limits<int64_t>::max() - fgBytes) > bgBytes
+                ? (fgBytes + bgBytes)
+                : std::numeric_limits<int64_t>::max();
+    }
+    int64_t sumWriteBytes() const {
+        const auto& [fgBytes, bgBytes] =
+                std::tuple(metrics[WRITE_BYTES][FOREGROUND], metrics[WRITE_BYTES][BACKGROUND]);
+        return (std::numeric_limits<int64_t>::max() - fgBytes) > bgBytes
+                ? (fgBytes + bgBytes)
+                : std::numeric_limits<int64_t>::max();
+    }
+    bool isZero() const;
+    std::string toString() const;
+    int64_t metrics[METRIC_TYPES][UID_STATES];
+};
+
+// Collector/Parser for `/proc/uid_io/stats`.
+class UidIoStatsCollectorInterface : public RefBase {
+public:
+    // Collects the per-UID I/O stats.
+    virtual android::base::Result<void> collect() = 0;
+    // Returns the latest per-uid I/O stats.
+    virtual const std::unordered_map<uid_t, UidIoStats> latestStats() const = 0;
+    // Returns the delta of per-uid I/O stats since the last before collection.
+    virtual const std::unordered_map<uid_t, UidIoStats> deltaStats() const = 0;
+    // Returns true only when the per-UID I/O stats file is accessible.
+    virtual bool enabled() const = 0;
+    // Returns the path for the per-UID I/O stats file.
+    virtual const std::string filePath() const = 0;
+};
+
+class UidIoStatsCollector final : public UidIoStatsCollectorInterface {
+public:
+    explicit UidIoStatsCollector(const std::string& path = kUidIoStatsPath) :
+          kEnabled(!access(path.c_str(), R_OK)), kPath(path) {}
+
+    ~UidIoStatsCollector() {}
+
+    android::base::Result<void> collect() override;
+
+    const std::unordered_map<uid_t, UidIoStats> latestStats() const override {
+        Mutex::Autolock lock(mMutex);
+        return mLatestStats;
+    }
+
+    const std::unordered_map<uid_t, UidIoStats> deltaStats() const override {
+        Mutex::Autolock lock(mMutex);
+        return mDeltaStats;
+    }
+
+    bool enabled() const override { return kEnabled; }
+
+    const std::string filePath() const override { return kPath; }
+
+private:
+    // Reads the contents of |kPath|.
+    android::base::Result<std::unordered_map<uid_t, UidIoStats>> readUidIoStatsLocked() const;
+
+    // Makes sure only one collection is running at any given time.
+    mutable Mutex mMutex;
+
+    // Latest dump from the file at |kPath|.
+    std::unordered_map<uid_t, UidIoStats> mLatestStats GUARDED_BY(mMutex);
+
+    // Delta of per-UID I/O stats since last before collection.
+    std::unordered_map<uid_t, UidIoStats> mDeltaStats GUARDED_BY(mMutex);
+
+    // True if kPath is accessible.
+    const bool kEnabled;
+
+    // Path to uid_io stats file. Default path is |kUidIoStatsPath|.
+    const std::string kPath;
+};
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
+
+#endif  //  CPP_WATCHDOG_SERVER_SRC_UIDIOSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/src/UidProcStatsCollector.cpp b/cpp/watchdog/server/src/UidProcStatsCollector.cpp
new file mode 100644
index 0000000..da3bffc
--- /dev/null
+++ b/cpp/watchdog/server/src/UidProcStatsCollector.cpp
@@ -0,0 +1,368 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#define LOG_TAG "carwatchdogd"
+#define DEBUG false  // STOPSHIP if true.
+
+#include "UidProcStatsCollector.h"
+
+#include <android-base/file.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <log/log.h>
+
+#include <dirent.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::base::EndsWith;
+using ::android::base::Error;
+using ::android::base::ParseInt;
+using ::android::base::ParseUint;
+using ::android::base::ReadFileToString;
+using ::android::base::Result;
+using ::android::base::Split;
+using ::android::base::StringAppendF;
+using ::android::base::Trim;
+
+namespace {
+
+enum ReadError {
+    ERR_INVALID_FILE = 0,
+    ERR_FILE_OPEN_READ = 1,
+    NUM_ERRORS = 2,
+};
+
+// Per-pid/tid stats.
+struct PidStat {
+    std::string comm = "";
+    std::string state = "";
+    uint64_t startTime = 0;
+    uint64_t majorFaults = 0;
+};
+
+/**
+ * /proc/PID/stat or /proc/PID/task/TID/stat format:
+ * <pid> <comm> <state> <ppid> <pgrp ID> <session ID> <tty_nr> <tpgid> <flags> <minor faults>
+ * <children minor faults> <major faults> <children major faults> <user mode time>
+ * <system mode time> <children user mode time> <children kernel mode time> <priority> <nice value>
+ * <num threads> <start time since boot> <virtual memory size> <resident set size> <rss soft limit>
+ * <start code addr> <end code addr> <start stack addr> <ESP value> <EIP> <bitmap of pending sigs>
+ * <bitmap of blocked sigs> <bitmap of ignored sigs> <waiting channel> <num pages swapped>
+ * <cumulative pages swapped> <exit signal> <processor #> <real-time prio> <agg block I/O delays>
+ * <guest time> <children guest time> <start data addr> <end data addr> <start break addr>
+ * <cmd line args start addr> <amd line args end addr> <env start addr> <env end addr> <exit code>
+ * Example line: 1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0 ...etc...
+ */
+bool parsePidStatLine(const std::string& line, PidStat* pidStat) {
+    std::vector<std::string> fields = Split(line, " ");
+
+    /* Note: Regex parsing for the below logic increased the time taken to run the
+     * UidProcStatsCollectorTest#TestProcPidStatContentsFromDevice from 151.7ms to 1.3 seconds.
+     *
+     * Comm string is enclosed with ( ) brackets and may contain space(s). Thus calculate the
+     * commEndOffset based on the field that contains the closing bracket.
+     */
+    size_t commEndOffset = 0;
+    for (size_t i = 1; i < fields.size(); ++i) {
+        pidStat->comm += fields[i];
+        if (EndsWith(fields[i], ")")) {
+            commEndOffset = i - 1;
+            break;
+        }
+        pidStat->comm += " ";
+    }
+
+    if (pidStat->comm.front() != '(' || pidStat->comm.back() != ')') {
+        ALOGD("Comm string `%s` not enclosed in brackets", pidStat->comm.c_str());
+        return false;
+    }
+    pidStat->comm.erase(pidStat->comm.begin());
+    pidStat->comm.erase(pidStat->comm.end() - 1);
+
+    if (fields.size() < 22 + commEndOffset ||
+        !ParseUint(fields[11 + commEndOffset], &pidStat->majorFaults) ||
+        !ParseUint(fields[21 + commEndOffset], &pidStat->startTime)) {
+        ALOGD("Invalid proc pid stat contents: \"%s\"", line.c_str());
+        return false;
+    }
+    pidStat->state = fields[2 + commEndOffset];
+    return true;
+}
+
+Result<void> readPidStatFile(const std::string& path, PidStat* pidStat) {
+    std::string buffer;
+    if (!ReadFileToString(path, &buffer)) {
+        return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
+    }
+    std::vector<std::string> lines = Split(std::move(buffer), "\n");
+    if (lines.size() != 1 && (lines.size() != 2 || !lines[1].empty())) {
+        return Error(ERR_INVALID_FILE) << path << " contains " << lines.size() << " lines != 1";
+    }
+    if (!parsePidStatLine(std::move(lines[0]), pidStat)) {
+        return Error(ERR_INVALID_FILE) << "Failed to parse the contents of " << path;
+    }
+    return {};
+}
+
+Result<std::unordered_map<std::string, std::string>> readKeyValueFile(
+        const std::string& path, const std::string& delimiter) {
+    std::string buffer;
+    if (!ReadFileToString(path, &buffer)) {
+        return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
+    }
+    std::unordered_map<std::string, std::string> contents;
+    std::vector<std::string> lines = Split(std::move(buffer), "\n");
+    for (size_t i = 0; i < lines.size(); ++i) {
+        if (lines[i].empty()) {
+            continue;
+        }
+        std::vector<std::string> elements = Split(lines[i], delimiter);
+        if (elements.size() < 2) {
+            return Error(ERR_INVALID_FILE)
+                    << "Line \"" << lines[i] << "\" doesn't contain the delimiter \"" << delimiter
+                    << "\" in file " << path;
+        }
+        std::string key = elements[0];
+        std::string value = Trim(lines[i].substr(key.length() + delimiter.length()));
+        if (contents.find(key) != contents.end()) {
+            return Error(ERR_INVALID_FILE)
+                    << "Duplicate " << key << " line: \"" << lines[i] << "\" in file " << path;
+        }
+        contents[key] = value;
+    }
+    return contents;
+}
+
+/**
+ * /proc/PID/status file format:
+ * Tgid:    <Thread group ID of the process>
+ * Uid:     <Read UID>   <Effective UID>   <Saved set UID>   <Filesystem UID>
+ *
+ * Note: Included only the fields that are parsed from the file.
+ */
+Result<std::tuple<uid_t, pid_t>> readPidStatusFile(const std::string& path) {
+    auto result = readKeyValueFile(path, ":\t");
+    if (!result.ok()) {
+        return Error(result.error().code()) << result.error();
+    }
+    auto contents = result.value();
+    if (contents.empty()) {
+        return Error(ERR_INVALID_FILE) << "Empty file " << path;
+    }
+    int64_t uid = 0;
+    int64_t tgid = 0;
+    if (contents.find("Uid") == contents.end() ||
+        !ParseInt(Split(contents["Uid"], "\t")[0], &uid)) {
+        return Error(ERR_INVALID_FILE) << "Failed to read 'UID' from file " << path;
+    }
+    if (contents.find("Tgid") == contents.end() || !ParseInt(contents["Tgid"], &tgid)) {
+        return Error(ERR_INVALID_FILE) << "Failed to read 'Tgid' from file " << path;
+    }
+    return std::make_tuple(uid, tgid);
+}
+
+}  // namespace
+
+std::string ProcessStats::toString() const {
+    return StringPrintf("{comm: %s, startTime: %" PRIu64 ", totalMajorFaults: %" PRIu64
+                        ", totalTasksCount: %d, ioBlockedTasksCount: %d}",
+                        comm.c_str(), startTime, totalMajorFaults, totalTasksCount,
+                        ioBlockedTasksCount);
+}
+
+std::string UidProcStats::toString() const {
+    std::string buffer;
+    StringAppendF(&buffer,
+                  "UidProcStats{totalMajorFaults: %" PRIu64 ", totalTasksCount: %d,"
+                  "ioBlockedTasksCount: %d, processStatsByPid: {",
+                  totalMajorFaults, totalTasksCount, ioBlockedTasksCount);
+    for (const auto& [pid, processStats] : processStatsByPid) {
+        StringAppendF(&buffer, "{pid: %" PRIi32 ", processStats: %s},", pid,
+                      processStats.toString().c_str());
+    }
+    StringAppendF(&buffer, "}");
+    return buffer;
+}
+
+Result<void> UidProcStatsCollector::collect() {
+    if (!mEnabled) {
+        return Error() << "Can not access PID stat files under " << kProcDirPath;
+    }
+
+    Mutex::Autolock lock(mMutex);
+    auto uidProcStatsByUid = readUidProcStatsLocked();
+    if (!uidProcStatsByUid.ok()) {
+        return Error() << uidProcStatsByUid.error();
+    }
+
+    mDeltaStats.clear();
+    for (const auto& [uid, currStats] : *uidProcStatsByUid) {
+        if (const auto& it = mLatestStats.find(uid); it == mLatestStats.end()) {
+            mDeltaStats[uid] = currStats;
+            continue;
+        }
+        const auto& prevStats = mLatestStats[uid];
+        UidProcStats deltaStats = {
+                .totalTasksCount = currStats.totalTasksCount,
+                .ioBlockedTasksCount = currStats.ioBlockedTasksCount,
+        };
+        for (const auto& [pid, processStats] : currStats.processStatsByPid) {
+            ProcessStats deltaProcessStats = processStats;
+            if (const auto& it = prevStats.processStatsByPid.find(pid);
+                it != prevStats.processStatsByPid.end() &&
+                it->second.startTime == processStats.startTime &&
+                it->second.totalMajorFaults <= deltaProcessStats.totalMajorFaults) {
+                deltaProcessStats.totalMajorFaults =
+                        deltaProcessStats.totalMajorFaults - it->second.totalMajorFaults;
+            }
+            deltaStats.totalMajorFaults += deltaProcessStats.totalMajorFaults;
+            deltaStats.processStatsByPid[pid] = deltaProcessStats;
+        }
+        mDeltaStats[uid] = std::move(deltaStats);
+    }
+    mLatestStats = std::move(*uidProcStatsByUid);
+    return {};
+}
+
+Result<std::unordered_map<uid_t, UidProcStats>> UidProcStatsCollector::readUidProcStatsLocked()
+        const {
+    std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid;
+    auto procDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(mPath.c_str()), closedir);
+    if (!procDirp) {
+        return Error() << "Failed to open " << mPath << " directory";
+    }
+    for (dirent* pidDir = nullptr; (pidDir = readdir(procDirp.get())) != nullptr;) {
+        pid_t pid = 0;
+        if (pidDir->d_type != DT_DIR || !ParseInt(pidDir->d_name, &pid)) {
+            continue;
+        }
+        auto result = readProcessStatsLocked(pid);
+        if (!result.ok()) {
+            if (result.error().code() != ERR_FILE_OPEN_READ) {
+                return Error() << result.error();
+            }
+            /* |ERR_FILE_OPEN_READ| is a soft-error because PID may disappear between scanning and
+             * reading directory/files.
+             */
+            if (DEBUG) {
+                ALOGD("%s", result.error().message().c_str());
+            }
+            continue;
+        }
+        uid_t uid = std::get<0>(*result);
+        ProcessStats processStats = std::get<ProcessStats>(*result);
+        if (uidProcStatsByUid.find(uid) == uidProcStatsByUid.end()) {
+            uidProcStatsByUid[uid] = {};
+        }
+        UidProcStats* uidProcStats = &uidProcStatsByUid[uid];
+        uidProcStats->totalMajorFaults += processStats.totalMajorFaults;
+        uidProcStats->totalTasksCount += processStats.totalTasksCount;
+        uidProcStats->ioBlockedTasksCount += processStats.ioBlockedTasksCount;
+        uidProcStats->processStatsByPid[pid] = std::move(processStats);
+    }
+    return uidProcStatsByUid;
+}
+
+Result<std::tuple<uid_t, ProcessStats>> UidProcStatsCollector::readProcessStatsLocked(
+        pid_t pid) const {
+    // 1. Read top-level pid stats.
+    PidStat pidStat = {};
+    std::string path = StringPrintf((mPath + kStatFileFormat).c_str(), pid);
+    if (auto result = readPidStatFile(path, &pidStat); !result.ok()) {
+        return Error(result.error().code())
+                << "Failed to read top-level per-process stat file '%s': %s"
+                << result.error().message().c_str();
+    }
+
+    // 2. Read aggregated process status.
+    pid_t tgid = -1;
+    uid_t uid = -1;
+    path = StringPrintf((mPath + kStatusFileFormat).c_str(), pid);
+    if (auto result = readPidStatusFile(path); !result.ok()) {
+        if (result.error().code() != ERR_FILE_OPEN_READ) {
+            return Error() << "Failed to read pid status for pid " << pid << ": "
+                           << result.error().message().c_str();
+        }
+        for (const auto& [curUid, uidProcStats] : mLatestStats) {
+            if (const auto it = uidProcStats.processStatsByPid.find(pid);
+                it != uidProcStats.processStatsByPid.end() &&
+                it->second.startTime == pidStat.startTime) {
+                tgid = pid;
+                uid = curUid;
+                break;
+            }
+        }
+    } else {
+        uid = std::get<0>(*result);
+        tgid = std::get<1>(*result);
+    }
+
+    if (uid == -1 || tgid != pid) {
+        return Error(ERR_FILE_OPEN_READ)
+                << "Skipping PID '" << pid << "' because either Tgid != PID or invalid UID";
+    }
+
+    ProcessStats processStats = {
+            .comm = std::move(pidStat.comm),
+            .startTime = pidStat.startTime,
+            .totalTasksCount = 1,
+            /* Top-level process stats has the aggregated major page faults count and this should be
+             * persistent across thread creation/termination. Thus use the value from this field.
+             */
+            .totalMajorFaults = pidStat.majorFaults,
+            .ioBlockedTasksCount = pidStat.state == "D" ? 1 : 0,
+    };
+
+    // 3. Read per-thread stats.
+    std::string taskDir = StringPrintf((mPath + kTaskDirFormat).c_str(), pid);
+    bool didReadMainThread = false;
+    auto taskDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(taskDir.c_str()), closedir);
+    for (dirent* tidDir = nullptr;
+         taskDirp != nullptr && (tidDir = readdir(taskDirp.get())) != nullptr;) {
+        pid_t tid = 0;
+        if (tidDir->d_type != DT_DIR || !ParseInt(tidDir->d_name, &tid) || tid == pid) {
+            continue;
+        }
+
+        PidStat tidStat = {};
+        path = StringPrintf((taskDir + kStatFileFormat).c_str(), tid);
+        if (const auto& result = readPidStatFile(path, &tidStat); !result.ok()) {
+            if (result.error().code() != ERR_FILE_OPEN_READ) {
+                return Error() << "Failed to read per-thread stat file: "
+                               << result.error().message().c_str();
+            }
+            /* Maybe the thread terminated before reading the file so skip this thread and
+             * continue with scanning the next thread's stat.
+             */
+            continue;
+        }
+        processStats.ioBlockedTasksCount += tidStat.state == "D" ? 1 : 0;
+        processStats.totalTasksCount += 1;
+    }
+    return std::make_tuple(uid, processStats);
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/watchdog/server/src/UidProcStatsCollector.h b/cpp/watchdog/server/src/UidProcStatsCollector.h
new file mode 100644
index 0000000..d0ec3c0
--- /dev/null
+++ b/cpp/watchdog/server/src/UidProcStatsCollector.h
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_SRC_UIDPROCSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_SRC_UIDPROCSTATSCOLLECTOR_H_
+
+#include <android-base/result.h>
+#include <android-base/stringprintf.h>
+#include <gtest/gtest_prod.h>
+#include <utils/Mutex.h>
+#include <utils/RefBase.h>
+
+#include <inttypes.h>
+#include <stdint.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::base::StringPrintf;
+
+#define PID_FOR_INIT 1
+
+constexpr const char kProcDirPath[] = "/proc";
+constexpr const char kStatFileFormat[] = "/%" PRIu32 "/stat";
+constexpr const char kTaskDirFormat[] = "/%" PRIu32 "/task";
+constexpr const char kStatusFileFormat[] = "/%" PRIu32 "/status";
+
+// Per-process stats.
+struct ProcessStats {
+    std::string comm = "";
+    uint64_t startTime = 0;  // Useful when identifying PID reuse
+    uint64_t totalMajorFaults = 0;
+    int totalTasksCount = 0;
+    int ioBlockedTasksCount = 0;
+    std::string toString() const;
+};
+
+// Per-UID stats.
+struct UidProcStats {
+    uint64_t totalMajorFaults = 0;
+    int totalTasksCount = 0;
+    int ioBlockedTasksCount = 0;
+    std::unordered_map<pid_t, ProcessStats> processStatsByPid = {};
+    std::string toString() const;
+};
+
+/**
+ * Collector/parser for `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat` and /proc/[pid]/status`
+ * files.
+ */
+class UidProcStatsCollectorInterface : public RefBase {
+public:
+    // Collects the per-uid stats from /proc directory.
+    virtual android::base::Result<void> collect() = 0;
+    // Returns the latest per-uid process stats.
+    virtual const std::unordered_map<uid_t, UidProcStats> latestStats() const = 0;
+    // Returns the delta of per-uid process stats since the last before collection.
+    virtual const std::unordered_map<uid_t, UidProcStats> deltaStats() const = 0;
+    // Returns true only when the /proc files for the init process are accessible.
+    virtual bool enabled() const = 0;
+    // Returns the /proc files common ancestor directory path.
+    virtual const std::string dirPath() const = 0;
+};
+
+class UidProcStatsCollector final : public UidProcStatsCollectorInterface {
+public:
+    explicit UidProcStatsCollector(const std::string& path = kProcDirPath) :
+          mLatestStats({}),
+          mPath(path) {
+        std::string pidStatPath = StringPrintf((mPath + kStatFileFormat).c_str(), PID_FOR_INIT);
+        std::string tidStatPath = StringPrintf((mPath + kTaskDirFormat + kStatFileFormat).c_str(),
+                                               PID_FOR_INIT, PID_FOR_INIT);
+        std::string pidStatusPath = StringPrintf((mPath + kStatusFileFormat).c_str(), PID_FOR_INIT);
+
+        mEnabled = !access(pidStatPath.c_str(), R_OK) && !access(tidStatPath.c_str(), R_OK) &&
+                !access(pidStatusPath.c_str(), R_OK);
+    }
+
+    ~UidProcStatsCollector() {}
+
+    android::base::Result<void> collect() override;
+
+    const std::unordered_map<uid_t, UidProcStats> latestStats() const {
+        Mutex::Autolock lock(mMutex);
+        return mLatestStats;
+    }
+
+    const std::unordered_map<uid_t, UidProcStats> deltaStats() const {
+        Mutex::Autolock lock(mMutex);
+        return mDeltaStats;
+    }
+
+    bool enabled() const { return mEnabled; }
+
+    const std::string dirPath() const { return mPath; }
+
+private:
+    android::base::Result<std::unordered_map<uid_t, UidProcStats>> readUidProcStatsLocked() const;
+
+    /**
+     * Reads the contents of the below files:
+     * 1. Pid stat file at |mPath| + |kStatFileFormat|
+     * 2. Aggregated per-process status at |mPath| + |kStatusFileFormat|
+     * 3. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
+     */
+    android::base::Result<std::tuple<uid_t, ProcessStats>> readProcessStatsLocked(pid_t pid) const;
+
+    // Makes sure only one collection is running at any given time.
+    mutable Mutex mMutex;
+
+    // Latest dump of per-UID stats.
+    std::unordered_map<uid_t, UidProcStats> mLatestStats GUARDED_BY(mMutex);
+
+    // Latest delta of per-uid stats.
+    std::unordered_map<uid_t, UidProcStats> mDeltaStats GUARDED_BY(mMutex);
+
+    /**
+     * True if the below files are accessible:
+     * 1. Pid stat file at |mPath| + |kTaskStatFileFormat|
+     * 2. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
+     * 3. Pid status file at |mPath| + |kStatusFileFormat|
+     * Otherwise, set to false.
+     */
+    bool mEnabled;
+
+    /**
+     * Proc directory path. Default value is |kProcDirPath|.
+     * Updated by tests to point to a different location when needed.
+     */
+    std::string mPath;
+
+    FRIEND_TEST(IoPerfCollectionTest, TestValidProcPidContents);
+    FRIEND_TEST(UidProcStatsCollectorTest, TestValidStatFiles);
+    FRIEND_TEST(UidProcStatsCollectorTest, TestHandlesProcessTerminationBetweenScanningAndParsing);
+    FRIEND_TEST(UidProcStatsCollectorTest, TestHandlesPidTidReuse);
+};
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
+
+#endif  //  CPP_WATCHDOG_SERVER_SRC_UIDPROCSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/src/UidStatsCollector.cpp b/cpp/watchdog/server/src/UidStatsCollector.cpp
new file mode 100644
index 0000000..19d0fc9
--- /dev/null
+++ b/cpp/watchdog/server/src/UidStatsCollector.cpp
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "carwatchdogd"
+
+#include "UidStatsCollector.h"
+
+#include <algorithm>
+#include <unordered_map>
+#include <unordered_set>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::sp;
+using ::android::base::Error;
+using ::android::base::Result;
+
+bool UidStats::hasPackageInfo() const {
+    return !packageInfo.packageIdentifier.name.empty();
+}
+
+uid_t UidStats::uid() const {
+    return static_cast<uid_t>(packageInfo.packageIdentifier.uid);
+}
+
+std::string UidStats::genericPackageName() const {
+    if (hasPackageInfo()) {
+        return packageInfo.packageIdentifier.name;
+    }
+    return std::to_string(packageInfo.packageIdentifier.uid);
+}
+
+Result<void> UidStatsCollector::collect() {
+    if (mUidProcStatsCollector->enabled()) {
+        if (const auto& result = mUidIoStatsCollector->collect(); !result.ok()) {
+            return Error() << "Failed to collect per-uid I/O stats: " << result.error();
+        }
+    }
+    if (mUidProcStatsCollector->enabled()) {
+        if (const auto& result = mUidProcStatsCollector->collect(); !result.ok()) {
+            return Error() << "Failed to collect per-uid process stats: " << result.error();
+        }
+    }
+    mLatestStats =
+            process(mUidIoStatsCollector->latestStats(), mUidProcStatsCollector->latestStats());
+    mDeltaStats = process(mUidIoStatsCollector->deltaStats(), mUidProcStatsCollector->deltaStats());
+    return {};
+}
+
+std::vector<UidStats> UidStatsCollector::process(
+        const std::unordered_map<uid_t, UidIoStats>& uidIoStatsByUid,
+        const std::unordered_map<uid_t, UidProcStats>& uidProcStatsByUid) const {
+    if (uidIoStatsByUid.empty() && uidProcStatsByUid.empty()) {
+        return std::vector<UidStats>();
+    }
+    std::unordered_set<uid_t> uidSet;
+    for (const auto& [uid, _] : uidIoStatsByUid) {
+        uidSet.insert(uid);
+    }
+    for (const auto& [uid, _] : uidProcStatsByUid) {
+        uidSet.insert(uid);
+    }
+    std::vector<uid_t> uids;
+    for (const auto& uid : uidSet) {
+        uids.push_back(uid);
+    }
+    const auto packageInfoByUid = mPackageInfoResolver->getPackageInfosForUids(uids);
+    std::vector<UidStats> uidStats;
+    for (const auto& uid : uids) {
+        UidStats curUidStats;
+        if (const auto it = packageInfoByUid.find(uid); it != packageInfoByUid.end()) {
+            curUidStats.packageInfo = it->second;
+        } else {
+            curUidStats.packageInfo.packageIdentifier.uid = uid;
+        }
+        if (const auto it = uidIoStatsByUid.find(uid); it != uidIoStatsByUid.end()) {
+            curUidStats.ioStats = it->second;
+        }
+        if (const auto it = uidProcStatsByUid.find(uid); it != uidProcStatsByUid.end()) {
+            curUidStats.procStats = it->second;
+        }
+        uidStats.emplace_back(std::move(curUidStats));
+    }
+    return uidStats;
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/watchdog/server/src/UidStatsCollector.h b/cpp/watchdog/server/src/UidStatsCollector.h
new file mode 100644
index 0000000..9a81abe
--- /dev/null
+++ b/cpp/watchdog/server/src/UidStatsCollector.h
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_SRC_UIDSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_SRC_UIDSTATSCOLLECTOR_H_
+
+#include "PackageInfoResolver.h"
+#include "UidIoStatsCollector.h"
+#include "UidProcStatsCollector.h"
+
+#include <android-base/result.h>
+#include <android/automotive/watchdog/internal/PackageInfo.h>
+#include <utils/Mutex.h>
+#include <utils/RefBase.h>
+#include <utils/StrongPointer.h>
+
+#include <string>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+// Forward declaration for testing use only.
+namespace internal {
+
+class UidStatsCollectorPeer;
+
+}  // namespace internal
+
+struct UidStats {
+    android::automotive::watchdog::internal::PackageInfo packageInfo;
+    UidIoStats ioStats = {};
+    UidProcStats procStats = {};
+    // Returns true when package info is available.
+    bool hasPackageInfo() const;
+    // Returns package name if the |packageInfo| is available. Otherwise, returns the |uid|.
+    std::string genericPackageName() const;
+    // Returns the uid for the stats;
+    uid_t uid() const;
+};
+
+// Collector/Aggregator for per-UID I/O and proc stats.
+class UidStatsCollectorInterface : public RefBase {
+public:
+    // Collects the per-UID I/O and proc stats.
+    virtual android::base::Result<void> collect() = 0;
+    // Returns the latest per-uid I/O and proc stats.
+    virtual const std::vector<UidStats> latestStats() const = 0;
+    // Returns the delta of per-uid I/O and proc stats since the last before collection.
+    virtual const std::vector<UidStats> deltaStats() const = 0;
+    // Returns true only when the per-UID I/O or proc stats files are accessible.
+    virtual bool enabled() const = 0;
+};
+
+class UidStatsCollector final : public UidStatsCollectorInterface {
+public:
+    UidStatsCollector() :
+          mPackageInfoResolver(PackageInfoResolver::getInstance()),
+          mUidIoStatsCollector(android::sp<UidIoStatsCollector>::make()),
+          mUidProcStatsCollector(android::sp<UidProcStatsCollector>::make()) {}
+
+    android::base::Result<void> collect() override;
+
+    const std::vector<UidStats> latestStats() const override {
+        Mutex::Autolock lock(mMutex);
+        return mLatestStats;
+    }
+
+    const std::vector<UidStats> deltaStats() const override {
+        Mutex::Autolock lock(mMutex);
+        return mDeltaStats;
+    }
+
+    bool enabled() const override {
+        return mUidIoStatsCollector->enabled() || mUidProcStatsCollector->enabled();
+    }
+
+private:
+    std::vector<UidStats> process(
+            const std::unordered_map<uid_t, UidIoStats>& uidIoStatsByUid,
+            const std::unordered_map<uid_t, UidProcStats>& uidProcStatsByUid) const;
+    // Local IPackageInfoResolver instance. Useful to mock in tests.
+    sp<IPackageInfoResolver> mPackageInfoResolver;
+
+    mutable Mutex mMutex;
+
+    android::sp<UidIoStatsCollectorInterface> mUidIoStatsCollector;
+
+    android::sp<UidProcStatsCollectorInterface> mUidProcStatsCollector;
+
+    std::vector<UidStats> mLatestStats;
+
+    std::vector<UidStats> mDeltaStats;
+
+    // For unit tests.
+    friend class internal::UidStatsCollectorPeer;
+};
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
+
+#endif  //  CPP_WATCHDOG_SERVER_SRC_UIDSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/src/WatchdogInternalHandler.cpp b/cpp/watchdog/server/src/WatchdogInternalHandler.cpp
index 6da5fb5..be49ff9 100644
--- a/cpp/watchdog/server/src/WatchdogInternalHandler.cpp
+++ b/cpp/watchdog/server/src/WatchdogInternalHandler.cpp
@@ -268,6 +268,15 @@
     return Status::ok();
 }
 
+Status WatchdogInternalHandler::controlProcessHealthCheck(bool disable) {
+    Status status = checkSystemUser();
+    if (!status.isOk()) {
+        return status;
+    }
+    mWatchdogProcessService->setEnabled(!disable);
+    return Status::ok();
+}
+
 }  // namespace watchdog
 }  // namespace automotive
 }  // namespace android
diff --git a/cpp/watchdog/server/src/WatchdogInternalHandler.h b/cpp/watchdog/server/src/WatchdogInternalHandler.h
index a2b4892..e23620e 100644
--- a/cpp/watchdog/server/src/WatchdogInternalHandler.h
+++ b/cpp/watchdog/server/src/WatchdogInternalHandler.h
@@ -91,8 +91,9 @@
                     configs) override;
     android::binder::Status actionTakenOnResourceOveruse(
             const std::vector<
-                    android::automotive::watchdog::internal::PackageResourceOveruseAction>&
-                    actions);
+                    android::automotive::watchdog::internal::PackageResourceOveruseAction>& actions)
+            override;
+    android::binder::Status controlProcessHealthCheck(bool disable) override;
 
 protected:
     void terminate() {
diff --git a/cpp/watchdog/server/src/WatchdogPerfService.cpp b/cpp/watchdog/server/src/WatchdogPerfService.cpp
index bf7480f..2761b34 100644
--- a/cpp/watchdog/server/src/WatchdogPerfService.cpp
+++ b/cpp/watchdog/server/src/WatchdogPerfService.cpp
@@ -370,7 +370,7 @@
                             << kEndCustomCollectionFlag << " flags";
 }
 
-Result<void> WatchdogPerfService::onDump(int fd) {
+Result<void> WatchdogPerfService::onDump(int fd) const {
     Mutex::Autolock lock(mMutex);
     if (mCurrCollectionEvent == EventType::TERMINATED) {
         ALOGW("%s not active. Dumping cached data", kServiceName);
@@ -407,7 +407,7 @@
     return {};
 }
 
-bool WatchdogPerfService::dumpHelpText(int fd) {
+bool WatchdogPerfService::dumpHelpText(int fd) const {
     return WriteStringToFd(StringPrintf(kHelpText, kServiceName, kStartCustomCollectionFlag,
                                         kIntervalFlag,
                                         std::chrono::duration_cast<std::chrono::seconds>(
@@ -421,12 +421,11 @@
                            fd);
 }
 
-Result<void> WatchdogPerfService::dumpCollectorsStatusLocked(int fd) {
-    if (!mUidIoStats->enabled() &&
-        !WriteStringToFd(StringPrintf("UidIoStats collector failed to access the file %s",
-                                      mUidIoStats->filePath().c_str()),
+Result<void> WatchdogPerfService::dumpCollectorsStatusLocked(int fd) const {
+    if (!mUidStatsCollector->enabled() &&
+        !WriteStringToFd(StringPrintf("UidStatsCollector failed to access proc and I/O files"),
                          fd)) {
-        return Error() << "Failed to write UidIoStats collector status";
+        return Error() << "Failed to write UidStatsCollector status";
     }
     if (!mProcStat->enabled() &&
         !WriteStringToFd(StringPrintf("ProcStat collector failed to access the file %s",
@@ -434,12 +433,6 @@
                          fd)) {
         return Error() << "Failed to write ProcStat collector status";
     }
-    if (!mProcPidStat->enabled() &&
-        !WriteStringToFd(StringPrintf("ProcPidStat collector failed to access the directory %s",
-                                      mProcPidStat->dirPath().c_str()),
-                         fd)) {
-        return Error() << "Failed to write ProcPidStat collector status";
-    }
     return {};
 }
 
@@ -620,15 +613,15 @@
 }
 
 Result<void> WatchdogPerfService::collectLocked(WatchdogPerfService::EventMetadata* metadata) {
-    if (!mUidIoStats->enabled() && !mProcStat->enabled() && !mProcPidStat->enabled()) {
+    if (!mUidStatsCollector->enabled() && !mProcStat->enabled()) {
         return Error() << "No collectors enabled";
     }
 
     time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
 
-    if (mUidIoStats->enabled()) {
-        if (const auto result = mUidIoStats->collect(); !result.ok()) {
-            return Error() << "Failed to collect per-uid I/O usage: " << result.error();
+    if (mUidStatsCollector->enabled()) {
+        if (const auto result = mUidStatsCollector->collect(); !result.ok()) {
+            return Error() << "Failed to collect per-uid proc and I/O stats: " << result.error();
         }
     }
 
@@ -638,25 +631,19 @@
         }
     }
 
-    if (mProcPidStat->enabled()) {
-        if (const auto result = mProcPidStat->collect(); !result.ok()) {
-            return Error() << "Failed to collect process stats: " << result.error();
-        }
-    }
-
     for (const auto& processor : mDataProcessors) {
         Result<void> result;
         switch (mCurrCollectionEvent) {
             case EventType::BOOT_TIME_COLLECTION:
-                result = processor->onBoottimeCollection(now, mUidIoStats, mProcStat, mProcPidStat);
+                result = processor->onBoottimeCollection(now, mUidStatsCollector, mProcStat);
                 break;
             case EventType::PERIODIC_COLLECTION:
-                result = processor->onPeriodicCollection(now, mSystemState, mUidIoStats, mProcStat,
-                                                         mProcPidStat);
+                result = processor->onPeriodicCollection(now, mSystemState, mUidStatsCollector,
+                                                         mProcStat);
                 break;
             case EventType::CUSTOM_COLLECTION:
                 result = processor->onCustomCollection(now, mSystemState, metadata->filterPackages,
-                                                       mUidIoStats, mProcStat, mProcPidStat);
+                                                       mUidStatsCollector, mProcStat);
                 break;
             default:
                 result = Error() << "Invalid collection event " << toString(mCurrCollectionEvent);
diff --git a/cpp/watchdog/server/src/WatchdogPerfService.h b/cpp/watchdog/server/src/WatchdogPerfService.h
index 7ff9e56..8fdf303 100644
--- a/cpp/watchdog/server/src/WatchdogPerfService.h
+++ b/cpp/watchdog/server/src/WatchdogPerfService.h
@@ -19,9 +19,8 @@
 
 #include "LooperWrapper.h"
 #include "ProcDiskStats.h"
-#include "ProcPidStat.h"
 #include "ProcStat.h"
-#include "UidIoStats.h"
+#include "UidStatsCollector.h"
 
 #include <android-base/chrono_utils.h>
 #include <android-base/result.h>
@@ -64,7 +63,7 @@
     GARAGE_MODE = 1,
 };
 
-/*
+/**
  * DataProcessor defines methods that must be implemented in order to process the data collected
  * by |WatchdogPerfService|.
  */
@@ -73,29 +72,30 @@
     IDataProcessorInterface() {}
     virtual ~IDataProcessorInterface() {}
     // Returns the name of the data processor.
-    virtual std::string name() = 0;
+    virtual std::string name() const = 0;
     // Callback to initialize the data processor.
     virtual android::base::Result<void> init() = 0;
     // Callback to terminate the data processor.
     virtual void terminate() = 0;
     // Callback to process the data collected during boot-time.
     virtual android::base::Result<void> onBoottimeCollection(
-            time_t time, const android::wp<UidIoStats>& uidIoStats,
-            const android::wp<ProcStat>& procStat, const android::wp<ProcPidStat>& procPidStat) = 0;
+            time_t time, const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::wp<ProcStat>& procStat) = 0;
     // Callback to process the data collected periodically post boot complete.
     virtual android::base::Result<void> onPeriodicCollection(
-            time_t time, SystemState systemState, const android::wp<UidIoStats>& uidIoStats,
-            const android::wp<ProcStat>& procStat, const android::wp<ProcPidStat>& procPidStat) = 0;
-    /*
+            time_t time, SystemState systemState,
+            const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::wp<ProcStat>& procStat) = 0;
+    /**
      * Callback to process the data collected on custom collection and filter the results only to
      * the specified |filterPackages|.
      */
     virtual android::base::Result<void> onCustomCollection(
             time_t time, SystemState systemState,
             const std::unordered_set<std::string>& filterPackages,
-            const android::wp<UidIoStats>& uidIoStats, const android::wp<ProcStat>& procStat,
-            const android::wp<ProcPidStat>& procPidStat) = 0;
-    /*
+            const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+            const android::wp<ProcStat>& procStat) = 0;
+    /**
      * Callback to periodically monitor the collected data and trigger the given |alertHandler|
      * on detecting resource overuse.
      */
@@ -103,8 +103,8 @@
             time_t time, const android::wp<IProcDiskStatsInterface>& procDiskStats,
             const std::function<void()>& alertHandler) = 0;
     // Callback to dump the boot-time collected and periodically collected data.
-    virtual android::base::Result<void> onDump(int fd) = 0;
-    /*
+    virtual android::base::Result<void> onDump(int fd) const = 0;
+    /**
      * Callback to dump the custom collected data. When fd == -1, clear the custom collection cache.
      */
     virtual android::base::Result<void> onCustomCollectionDump(int fd) = 0;
@@ -127,20 +127,20 @@
 };
 
 enum SwitchMessage {
-    /*
+    /**
      * On receiving this message, collect the last boot-time record and start periodic collection
      * and monitor.
      */
     END_BOOTTIME_COLLECTION = EventType::LAST_EVENT + 1,
 
-    /*
+    /**
      * On receiving this message, ends custom collection, discard collected data and start periodic
      * collection and monitor.
      */
     END_CUSTOM_COLLECTION,
 };
 
-/*
+/**
  * WatchdogPerfServiceInterface collects performance data during boot-time and periodically post
  * boot complete. It exposes APIs that the main thread and binder service can call to start a
  * collection, switch the collection type, and generate collection dumps.
@@ -150,7 +150,7 @@
     // Register a data processor to process the data collected by |WatchdogPerfService|.
     virtual android::base::Result<void> registerDataProcessor(
             android::sp<IDataProcessorInterface> processor) = 0;
-    /*
+    /**
      * Starts the boot-time collection in the looper handler on a new thread and returns
      * immediately. Must be called only once. Otherwise, returns an error.
      */
@@ -161,7 +161,7 @@
     virtual void setSystemState(SystemState systemState) = 0;
     // Ends the boot-time collection by switching to periodic collection and returns immediately.
     virtual android::base::Result<void> onBootFinished() = 0;
-    /*
+    /**
      * Depending on the arguments, it either:
      * 1. Starts a custom collection.
      * 2. Or ends the current custom collection and dumps the collected data.
@@ -170,9 +170,9 @@
     virtual android::base::Result<void> onCustomCollection(
             int fd, const Vector<android::String16>& args) = 0;
     // Generates a dump from the boot-time and periodic collection events.
-    virtual android::base::Result<void> onDump(int fd) = 0;
+    virtual android::base::Result<void> onDump(int fd) const = 0;
     // Dumps the help text.
-    virtual bool dumpHelpText(int fd) = 0;
+    virtual bool dumpHelpText(int fd) const = 0;
 };
 
 class WatchdogPerfService final : public WatchdogPerfServiceInterface {
@@ -185,9 +185,8 @@
           mCustomCollection({}),
           mPeriodicMonitor({}),
           mCurrCollectionEvent(EventType::INIT),
-          mUidIoStats(android::sp<UidIoStats>::make()),
+          mUidStatsCollector(android::sp<UidStatsCollector>::make()),
           mProcStat(android::sp<ProcStat>::make()),
-          mProcPidStat(android::sp<ProcPidStat>::make()),
           mProcDiskStats(android::sp<ProcDiskStats>::make()),
           mDataProcessors({}) {}
 
@@ -207,9 +206,9 @@
     android::base::Result<void> onCustomCollection(int fd,
                                                    const Vector<android::String16>& args) override;
 
-    android::base::Result<void> onDump(int fd) override;
+    android::base::Result<void> onDump(int fd) const override;
 
-    bool dumpHelpText(int fd) override;
+    bool dumpHelpText(int fd) const override;
 
 private:
     struct EventMetadata {
@@ -226,9 +225,9 @@
     };
 
     // Dumps the collectors' status when they are disabled.
-    android::base::Result<void> dumpCollectorsStatusLocked(int fd);
+    android::base::Result<void> dumpCollectorsStatusLocked(int fd) const;
 
-    /*
+    /**
      * Starts a custom collection on the looper handler, temporarily stops the periodic collection
      * (won't discard the collected data), and returns immediately. Returns any error observed
      * during this process.
@@ -243,7 +242,7 @@
             std::chrono::nanoseconds interval, std::chrono::nanoseconds maxDuration,
             const std::unordered_set<std::string>& filterPackages);
 
-    /*
+    /**
      * Ends the current custom collection, generates a dump, sends a looper message to start the
      * periodic collection, and returns immediately. Returns an error when there is no custom
      * collection running or when a dump couldn't be generated from the custom collection.
@@ -262,7 +261,7 @@
     // Processes the monitor events received by |handleMessage|.
     android::base::Result<void> processMonitorEvent(EventMetadata* metadata);
 
-    /*
+    /**
      * Returns the metadata for the current collection based on |mCurrCollectionEvent|. Returns
      * nullptr on invalid collection event.
      */
@@ -272,7 +271,7 @@
     std::thread mCollectionThread;
 
     // Makes sure only one collection is running at any given time.
-    Mutex mMutex;
+    mutable Mutex mMutex;
 
     // Handler lopper to execute different collection events on the collection thread.
     android::sp<LooperWrapper> mHandlerLooper GUARDED_BY(mMutex);
@@ -295,21 +294,18 @@
     // Info for the |EventType::PERIODIC| monitor event.
     EventMetadata mPeriodicMonitor GUARDED_BY(mMutex);
 
-    /*
+    /**
      * Tracks either the WatchdogPerfService's state or current collection event. Updated on
      * |start|, |onBootComplete|, |startCustomCollection|, |endCustomCollection|, and |terminate|.
      */
     EventType mCurrCollectionEvent GUARDED_BY(mMutex);
 
-    // Collector/parser for `/proc/uid_io/stats`.
-    android::sp<UidIoStats> mUidIoStats GUARDED_BY(mMutex);
+    // Collector for UID process and I/O stats.
+    android::sp<UidStatsCollectorInterface> mUidStatsCollector GUARDED_BY(mMutex);
 
     // Collector/parser for `/proc/stat`.
     android::sp<ProcStat> mProcStat GUARDED_BY(mMutex);
 
-    // Collector/parser for `/proc/PID/*` stat files.
-    android::sp<ProcPidStat> mProcPidStat GUARDED_BY(mMutex);
-
     // Collector/parser for `/proc/diskstats` file.
     android::sp<IProcDiskStatsInterface> mProcDiskStats GUARDED_BY(mMutex);
 
diff --git a/cpp/watchdog/server/src/WatchdogProcessService.cpp b/cpp/watchdog/server/src/WatchdogProcessService.cpp
index 97a4047..fca1174 100644
--- a/cpp/watchdog/server/src/WatchdogProcessService.cpp
+++ b/cpp/watchdog/server/src/WatchdogProcessService.cpp
@@ -83,10 +83,11 @@
 const int32_t MSG_VHAL_WATCHDOG_ALIVE = static_cast<int>(TimeoutLength::TIMEOUT_NORMAL) + 1;
 const int32_t MSG_VHAL_HEALTH_CHECK = MSG_VHAL_WATCHDOG_ALIVE + 1;
 
-// VHAL sends heart beat every 3s. Car watchdog checks if there is the latest heart beat from VHAL
+// TODO(b/193742550): Restore the timeout to 3s after configuration by vendors is added.
+// VHAL sends heart beat every 6s. Car watchdog checks if there is the latest heart beat from VHAL
 // with 1s marginal time.
-constexpr std::chrono::nanoseconds kVhalHealthCheckDelayNs = 4s;
-constexpr int64_t kVhalHeartBeatIntervalMs = 3000;
+constexpr std::chrono::milliseconds kVhalHeartBeatIntervalMs = 6s;
+constexpr std::chrono::nanoseconds kVhalHealthCheckDelayNs = kVhalHeartBeatIntervalMs + 1s;
 
 constexpr const char kServiceName[] = "WatchdogProcessService";
 constexpr const char kVhalInterfaceName[] = "android.hardware.automotive.vehicle@2.0::IVehicle";
@@ -799,7 +800,7 @@
         Mutex::Autolock lock(mMutex);
         lastEventTime = mVhalHeartBeat.eventTime;
     }
-    if (currentUptime > lastEventTime + kVhalHeartBeatIntervalMs) {
+    if (currentUptime > lastEventTime + kVhalHeartBeatIntervalMs.count()) {
         ALOGW("VHAL failed to update heart beat within timeout. Terminating VHAL...");
         terminateVhal();
     }
diff --git a/cpp/watchdog/server/tests/IoOveruseConfigsTest.cpp b/cpp/watchdog/server/tests/IoOveruseConfigsTest.cpp
index 79b32fc..8eefeb4 100644
--- a/cpp/watchdog/server/tests/IoOveruseConfigsTest.cpp
+++ b/cpp/watchdog/server/tests/IoOveruseConfigsTest.cpp
@@ -17,6 +17,7 @@
 #include "IoOveruseConfigs.h"
 #include "OveruseConfigurationTestUtils.h"
 #include "OveruseConfigurationXmlHelper.h"
+#include "PackageInfoTestUtils.h"
 
 #include <android-base/strings.h>
 #include <gmock/gmock.h>
@@ -73,17 +74,6 @@
     return mappings;
 }
 
-PackageInfo constructPackageInfo(
-        const char* packageName, const ComponentType componentType,
-        const ApplicationCategoryType appCategoryType = ApplicationCategoryType::OTHERS) {
-    PackageInfo packageInfo;
-    packageInfo.packageIdentifier.name = packageName;
-    packageInfo.uidType = UidType::APPLICATION;
-    packageInfo.componentType = componentType;
-    packageInfo.appCategoryType = appCategoryType;
-    return packageInfo;
-}
-
 std::string toString(std::vector<ResourceOveruseConfiguration> configs) {
     std::string buffer;
     StringAppendF(&buffer, "[");
@@ -97,9 +87,9 @@
     return buffer;
 }
 
-std::vector<Matcher<const ResourceOveruseConfiguration>> ResourceOveruseConfigurationsMatchers(
+std::vector<Matcher<const ResourceOveruseConfiguration&>> ResourceOveruseConfigurationsMatchers(
         const std::vector<ResourceOveruseConfiguration>& configs) {
-    std::vector<Matcher<const ResourceOveruseConfiguration>> matchers;
+    std::vector<Matcher<const ResourceOveruseConfiguration&>> matchers;
     for (const auto config : configs) {
         matchers.push_back(ResourceOveruseConfigurationMatcher(config));
     }
@@ -524,21 +514,21 @@
     PerStateBytes defaultPerStateBytes = defaultThreshold().perStateWriteBytes;
     IoOveruseConfigs ioOveruseConfigs;
 
-    auto packageInfo = constructPackageInfo("systemPackage", ComponentType::SYSTEM);
+    auto packageInfo = constructAppPackageInfo("systemPackage", ComponentType::SYSTEM);
     EXPECT_THAT(ioOveruseConfigs.fetchThreshold(packageInfo), defaultPerStateBytes)
             << "System package should have default threshold";
     EXPECT_FALSE(ioOveruseConfigs.isSafeToKill(packageInfo))
             << "System package shouldn't be killed by default";
 
-    packageInfo = constructPackageInfo("vendorPackage", ComponentType::VENDOR,
-                                       ApplicationCategoryType::MEDIA);
+    packageInfo = constructAppPackageInfo("vendorPackage", ComponentType::VENDOR,
+                                          ApplicationCategoryType::MEDIA);
     EXPECT_THAT(ioOveruseConfigs.fetchThreshold(packageInfo), defaultPerStateBytes)
             << "Vendor package should have default threshold";
     EXPECT_FALSE(ioOveruseConfigs.isSafeToKill(packageInfo))
             << "Vendor package shouldn't be killed by default";
 
-    packageInfo = constructPackageInfo("3pPackage", ComponentType::THIRD_PARTY,
-                                       ApplicationCategoryType::MAPS);
+    packageInfo = constructAppPackageInfo("3pPackage", ComponentType::THIRD_PARTY,
+                                          ApplicationCategoryType::MAPS);
     EXPECT_THAT(ioOveruseConfigs.fetchThreshold(packageInfo), defaultPerStateBytes)
             << "Third-party package should have default threshold";
     EXPECT_TRUE(ioOveruseConfigs.isSafeToKill(packageInfo))
@@ -808,68 +798,112 @@
     const auto ioOveruseConfigs = sampleIoOveruseConfigs();
 
     auto actual = ioOveruseConfigs->fetchThreshold(
-            constructPackageInfo("systemPackageGeneric", ComponentType::SYSTEM));
+            constructAppPackageInfo("systemPackageGeneric", ComponentType::SYSTEM));
 
     EXPECT_THAT(actual, SYSTEM_COMPONENT_LEVEL_THRESHOLDS);
 
     actual = ioOveruseConfigs->fetchThreshold(
-            constructPackageInfo("systemPackageA", ComponentType::SYSTEM));
+            constructAppPackageInfo("systemPackageA", ComponentType::SYSTEM));
 
     EXPECT_THAT(actual, SYSTEM_PACKAGE_A_THRESHOLDS);
 
-    actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("systemPackageB",
-                                                                   ComponentType::SYSTEM,
-                                                                   ApplicationCategoryType::MEDIA));
+    actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("systemPackageB", ComponentType::SYSTEM,
+                                    ApplicationCategoryType::MEDIA));
 
     // Package specific thresholds get priority over media category thresholds.
     EXPECT_THAT(actual, SYSTEM_PACKAGE_B_THRESHOLDS);
 
-    actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("systemPackageC",
-                                                                   ComponentType::SYSTEM,
-                                                                   ApplicationCategoryType::MEDIA));
+    actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("systemPackageC", ComponentType::SYSTEM,
+                                    ApplicationCategoryType::MEDIA));
 
     // Media category thresholds as there is no package specific thresholds.
     EXPECT_THAT(actual, MEDIA_THRESHOLDS);
 }
 
+TEST_F(IoOveruseConfigsTest, TestFetchThresholdForSharedSystemPackages) {
+    const auto ioOveruseConfigs = sampleIoOveruseConfigs();
+    auto sampleSystemConfig = sampleUpdateSystemConfig();
+    auto& ioConfig = sampleSystemConfig.resourceSpecificConfigurations[0]
+                             .get<ResourceSpecificConfiguration::ioOveruseConfiguration>();
+    ioConfig.packageSpecificThresholds.push_back(
+            toPerStateIoOveruseThreshold("shared:systemSharedPackage",
+                                         toPerStateBytes(100, 200, 300)));
+
+    ioOveruseConfigs->update({sampleSystemConfig});
+
+    auto actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("shared:systemSharedPackage", ComponentType::SYSTEM));
+
+    EXPECT_THAT(actual, toPerStateBytes(100, 200, 300));
+
+    actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("systemSharedPackage", ComponentType::SYSTEM));
+
+    EXPECT_THAT(actual, SYSTEM_COMPONENT_LEVEL_THRESHOLDS);
+}
+
 TEST_F(IoOveruseConfigsTest, TestFetchThresholdForVendorPackages) {
     const auto ioOveruseConfigs = sampleIoOveruseConfigs();
 
     auto actual = ioOveruseConfigs->fetchThreshold(
-            constructPackageInfo("vendorPackageGeneric", ComponentType::VENDOR));
+            constructAppPackageInfo("vendorPackageGeneric", ComponentType::VENDOR));
 
     EXPECT_THAT(actual, VENDOR_COMPONENT_LEVEL_THRESHOLDS);
 
     actual = ioOveruseConfigs->fetchThreshold(
-            constructPackageInfo("vendorPkgB", ComponentType::VENDOR));
+            constructAppPackageInfo("vendorPkgB", ComponentType::VENDOR));
 
     EXPECT_THAT(actual, VENDOR_PKG_B_THRESHOLDS);
 
-    actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("vendorPackageC",
-                                                                   ComponentType::VENDOR,
-                                                                   ApplicationCategoryType::MAPS));
+    actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("vendorPackageC", ComponentType::VENDOR,
+                                    ApplicationCategoryType::MAPS));
 
     // Maps category thresholds as there is no package specific thresholds.
     EXPECT_THAT(actual, MAPS_THRESHOLDS);
 }
 
+TEST_F(IoOveruseConfigsTest, TestFetchThresholdForSharedVendorPackages) {
+    const auto ioOveruseConfigs = sampleIoOveruseConfigs();
+    auto sampleVendorConfig = sampleUpdateVendorConfig();
+    auto& ioConfig = sampleVendorConfig.resourceSpecificConfigurations[0]
+                             .get<ResourceSpecificConfiguration::ioOveruseConfiguration>();
+    ioConfig.packageSpecificThresholds.push_back(
+            toPerStateIoOveruseThreshold("shared:vendorSharedPackage",
+                                         toPerStateBytes(100, 200, 300)));
+
+    ioOveruseConfigs->update({sampleVendorConfig});
+
+    auto actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("shared:vendorSharedPackage", ComponentType::VENDOR));
+
+    EXPECT_THAT(actual, toPerStateBytes(100, 200, 300));
+
+    actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("vendorSharedPackage", ComponentType::VENDOR));
+
+    EXPECT_THAT(actual, VENDOR_COMPONENT_LEVEL_THRESHOLDS);
+}
+
 TEST_F(IoOveruseConfigsTest, TestFetchThresholdForThirdPartyPackages) {
     const auto ioOveruseConfigs = sampleIoOveruseConfigs();
 
     auto actual = ioOveruseConfigs->fetchThreshold(
-            constructPackageInfo("vendorPackageGenericImpostor", ComponentType::THIRD_PARTY));
+            constructAppPackageInfo("vendorPackageGenericImpostor", ComponentType::THIRD_PARTY));
 
     EXPECT_THAT(actual, THIRD_PARTY_COMPONENT_LEVEL_THRESHOLDS);
 
-    actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("3pMapsPackage",
-                                                                   ComponentType::THIRD_PARTY,
-                                                                   ApplicationCategoryType::MAPS));
+    actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("3pMapsPackage", ComponentType::THIRD_PARTY,
+                                    ApplicationCategoryType::MAPS));
 
     EXPECT_THAT(actual, MAPS_THRESHOLDS);
 
-    actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("3pMediaPackage",
-                                                                   ComponentType::THIRD_PARTY,
-                                                                   ApplicationCategoryType::MEDIA));
+    actual = ioOveruseConfigs->fetchThreshold(
+            constructAppPackageInfo("3pMediaPackage", ComponentType::THIRD_PARTY,
+                                    ApplicationCategoryType::MEDIA));
 
     EXPECT_THAT(actual, MEDIA_THRESHOLDS);
 }
@@ -877,29 +911,93 @@
 TEST_F(IoOveruseConfigsTest, TestIsSafeToKillSystemPackages) {
     const auto ioOveruseConfigs = sampleIoOveruseConfigs();
     EXPECT_FALSE(ioOveruseConfigs->isSafeToKill(
-            constructPackageInfo("systemPackageGeneric", ComponentType::SYSTEM)));
+            constructAppPackageInfo("systemPackageGeneric", ComponentType::SYSTEM)));
 
     EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(
-            constructPackageInfo("systemPackageA", ComponentType::SYSTEM)));
+            constructAppPackageInfo("systemPackageA", ComponentType::SYSTEM)));
+}
+
+TEST_F(IoOveruseConfigsTest, TestIsSafeToKillSharedSystemPackages) {
+    auto sampleSystemConfig = sampleUpdateSystemConfig();
+    sampleSystemConfig.safeToKillPackages.push_back("sharedUidSystemPackageC");
+    sampleSystemConfig.safeToKillPackages.push_back("shared:systemSharedPackageD");
+    sp<IoOveruseConfigs> ioOveruseConfigs = new IoOveruseConfigs();
+
+    EXPECT_RESULT_OK(ioOveruseConfigs->update({sampleSystemConfig}));
+
+    PackageInfo packageInfo =
+            constructAppPackageInfo("systemSharedPackage", ComponentType::SYSTEM,
+                                    ApplicationCategoryType::OTHERS,
+                                    {"sharedUidSystemPackageA", "sharedUidSystemPackageB",
+                                     "sharedUidSystemPackageC"});
+
+    EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(packageInfo))
+            << "Should be safe-to-kill when at least one package under shared UID is safe-to-kill";
+
+    packageInfo =
+            constructAppPackageInfo("shared:systemSharedPackageD", ComponentType::SYSTEM,
+                                    ApplicationCategoryType::OTHERS, {"sharedUidSystemPackageA"});
+    EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(packageInfo))
+            << "Should be safe-to-kill when shared package is safe-to-kill";
+
+    packageInfo =
+            constructAppPackageInfo("systemSharedPackageD", ComponentType::SYSTEM,
+                                    ApplicationCategoryType::OTHERS, {"sharedUidSystemPackageA"});
+    EXPECT_FALSE(ioOveruseConfigs->isSafeToKill(packageInfo))
+            << "Shouldn't be safe-to-kill when the 'shared:' prefix is missing";
 }
 
 TEST_F(IoOveruseConfigsTest, TestIsSafeToKillVendorPackages) {
     const auto ioOveruseConfigs = sampleIoOveruseConfigs();
     EXPECT_FALSE(ioOveruseConfigs->isSafeToKill(
-            constructPackageInfo("vendorPackageGeneric", ComponentType::VENDOR)));
+            constructAppPackageInfo("vendorPackageGeneric", ComponentType::VENDOR)));
 
     EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(
-            constructPackageInfo("vendorPackageA", ComponentType::VENDOR)));
+            constructAppPackageInfo("vendorPackageA", ComponentType::VENDOR)));
+}
+
+TEST_F(IoOveruseConfigsTest, TestIsSafeToKillSharedVendorPackages) {
+    auto sampleVendorConfig = sampleUpdateVendorConfig();
+    sampleVendorConfig.safeToKillPackages.push_back("sharedUidVendorPackageC");
+    sampleVendorConfig.safeToKillPackages.push_back("shared:vendorSharedPackageD");
+
+    auto sampleSystemConfig = sampleUpdateSystemConfig();
+    sampleSystemConfig.safeToKillPackages.push_back("sharedUidSystemPackageC");
+
+    sp<IoOveruseConfigs> ioOveruseConfigs = new IoOveruseConfigs();
+
+    EXPECT_RESULT_OK(ioOveruseConfigs->update({sampleSystemConfig, sampleVendorConfig}));
+
+    PackageInfo packageInfo =
+            constructAppPackageInfo("vendorSharedPackage", ComponentType::VENDOR,
+                                    ApplicationCategoryType::OTHERS,
+                                    {"sharedUidVendorPackageA", "sharedUidVendorPackageB",
+                                     "sharedUidVendorPackageC"});
+
+    EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(packageInfo))
+            << "Should be safe-to-kill when at least one package under shared UID is safe-to-kill";
+
+    packageInfo =
+            constructAppPackageInfo("shared:vendorSharedPackageD", ComponentType::VENDOR,
+                                    ApplicationCategoryType::OTHERS, {"sharedUidVendorPackageA"});
+    EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(packageInfo))
+            << "Should be safe-to-kill when shared package is safe-to-kill";
+
+    packageInfo =
+            constructAppPackageInfo("shared:vendorSharedPackageE", ComponentType::VENDOR,
+                                    ApplicationCategoryType::OTHERS, {"sharedUidVendorPackageA"});
+    EXPECT_FALSE(ioOveruseConfigs->isSafeToKill(packageInfo))
+            << "Shouldn't be safe-to-kill when the 'shared:' prefix is missing";
 }
 
 TEST_F(IoOveruseConfigsTest, TestIsSafeToKillThirdPartyPackages) {
     const auto ioOveruseConfigs = sampleIoOveruseConfigs();
     EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(
-            constructPackageInfo("vendorPackageGenericImpostor", ComponentType::THIRD_PARTY)));
+            constructAppPackageInfo("vendorPackageGenericImpostor", ComponentType::THIRD_PARTY)));
 
     EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(
-            constructPackageInfo("3pMapsPackage", ComponentType::THIRD_PARTY,
-                                 ApplicationCategoryType::MAPS)));
+            constructAppPackageInfo("3pMapsPackage", ComponentType::THIRD_PARTY,
+                                    ApplicationCategoryType::MAPS)));
 }
 
 TEST_F(IoOveruseConfigsTest, TestIsSafeToKillNativePackages) {
@@ -931,6 +1029,32 @@
                 UnorderedElementsAre("vendorPackage", "vendorPkgB"));
 }
 
+TEST_F(IoOveruseConfigsTest, TestVendorPackagePrefixesWithSharedPackages) {
+    auto sampleVendorConfig = sampleUpdateVendorConfig();
+    sampleVendorConfig.vendorPackagePrefixes.push_back("shared:vendorSharedPackage");
+    sampleVendorConfig.safeToKillPackages.push_back("sharedUidVendorPackageD");
+    sampleVendorConfig.safeToKillPackages.push_back("shared:vendorSharedPackageE");
+    sampleVendorConfig.safeToKillPackages.push_back("shared:vndrSharedPkgF");
+
+    auto& ioConfig = sampleVendorConfig.resourceSpecificConfigurations[0]
+                             .get<ResourceSpecificConfiguration::ioOveruseConfiguration>();
+
+    ioConfig.packageSpecificThresholds.push_back(
+            toPerStateIoOveruseThreshold("shared:vendorSharedPackageG",
+                                         VENDOR_PACKAGE_A_THRESHOLDS));
+    ioConfig.packageSpecificThresholds.push_back(
+            toPerStateIoOveruseThreshold("shared:vndrSharedPkgH", VENDOR_PACKAGE_A_THRESHOLDS));
+
+    sp<IoOveruseConfigs> ioOveruseConfigs = new IoOveruseConfigs();
+
+    EXPECT_RESULT_OK(ioOveruseConfigs->update({sampleVendorConfig}));
+
+    EXPECT_THAT(ioOveruseConfigs->vendorPackagePrefixes(),
+                UnorderedElementsAre("vendorPackage", "vendorPkgB", "shared:vendorSharedPackage",
+                                     "sharedUidVendorPackageD", "shared:vndrSharedPkgF",
+                                     "shared:vndrSharedPkgH"));
+}
+
 TEST_F(IoOveruseConfigsTest, TestPackagesToAppCategoriesWithSystemConfig) {
     IoOveruseConfigs ioOveruseConfigs;
     const auto resourceOveruseConfig = sampleUpdateSystemConfig();
diff --git a/cpp/watchdog/server/tests/IoOveruseMonitorTest.cpp b/cpp/watchdog/server/tests/IoOveruseMonitorTest.cpp
index 7dcc2d4..76f3cbb 100644
--- a/cpp/watchdog/server/tests/IoOveruseMonitorTest.cpp
+++ b/cpp/watchdog/server/tests/IoOveruseMonitorTest.cpp
@@ -19,14 +19,17 @@
 #include "MockPackageInfoResolver.h"
 #include "MockProcDiskStats.h"
 #include "MockResourceOveruseListener.h"
-#include "MockUidIoStats.h"
+#include "MockUidStatsCollector.h"
 #include "MockWatchdogServiceHelper.h"
+#include "PackageInfoTestUtils.h"
 
 #include <binder/IPCThreadState.h>
 #include <binder/Status.h>
 #include <utils/RefBase.h>
 
 #include <functional>
+#include <tuple>
+#include <unordered_map>
 
 namespace android {
 namespace automotive {
@@ -47,6 +50,7 @@
 using ::android::binder::Status;
 using ::testing::_;
 using ::testing::DoAll;
+using ::testing::Eq;
 using ::testing::Return;
 using ::testing::ReturnRef;
 using ::testing::SaveArg;
@@ -68,20 +72,11 @@
     return threshold;
 }
 
-PackageIdentifier constructPackageIdentifier(const char* packageName, const int32_t uid) {
-    PackageIdentifier packageIdentifier;
-    packageIdentifier.name = packageName;
-    packageIdentifier.uid = uid;
-    return packageIdentifier;
-}
-
-PackageInfo constructPackageInfo(const char* packageName, const int32_t uid,
-                                 const UidType uidType) {
+struct PackageWrittenBytes {
     PackageInfo packageInfo;
-    packageInfo.packageIdentifier = constructPackageIdentifier(packageName, uid);
-    packageInfo.uidType = uidType;
-    return packageInfo;
-}
+    int32_t foregroundBytes;
+    int32_t backgroundBytes;
+};
 
 PerStateBytes constructPerStateBytes(const int64_t fgBytes, const int64_t bgBytes,
                                      const int64_t gmBytes) {
@@ -196,42 +191,25 @@
         mMockWatchdogServiceHelper = sp<MockWatchdogServiceHelper>::make();
         mMockIoOveruseConfigs = sp<MockIoOveruseConfigs>::make();
         mMockPackageInfoResolver = sp<MockPackageInfoResolver>::make();
+        mMockUidStatsCollector = sp<MockUidStatsCollector>::make();
         mIoOveruseMonitor = sp<IoOveruseMonitor>::make(mMockWatchdogServiceHelper);
         mIoOveruseMonitorPeer = sp<internal::IoOveruseMonitorPeer>::make(mIoOveruseMonitor);
         mIoOveruseMonitorPeer->init(mMockIoOveruseConfigs, mMockPackageInfoResolver);
+        setUpPackagesAndConfigurations();
     }
 
     virtual void TearDown() {
         mMockWatchdogServiceHelper.clear();
         mMockIoOveruseConfigs.clear();
         mMockPackageInfoResolver.clear();
+        mMockUidStatsCollector.clear();
         mIoOveruseMonitor.clear();
         mIoOveruseMonitorPeer.clear();
     }
 
     void setUpPackagesAndConfigurations() {
-        std::unordered_map<uid_t, PackageInfo> packageInfoMapping =
-                {{1001000,
-                  constructPackageInfo(
-                          /*packageName=*/"system.daemon", /*uid=*/1001000, UidType::NATIVE)},
-                 {1112345,
-                  constructPackageInfo(
-                          /*packageName=*/"com.android.google.package", /*uid=*/1112345,
-                          UidType::APPLICATION)},
-                 {1113999,
-                  constructPackageInfo(
-                          /*packageName=*/"com.android.google.package", /*uid=*/1113999,
-                          UidType::APPLICATION)},
-                 {1212345,
-                  constructPackageInfo(
-                          /*packageName=*/"com.android.google.package", /*uid=*/1212345,
-                          UidType::APPLICATION)},
-                 {1312345,
-                  constructPackageInfo(
-                          /*packageName=*/"com.android.google.package", /*uid=*/1312345,
-                          UidType::APPLICATION)}};
         ON_CALL(*mMockPackageInfoResolver, getPackageInfosForUids(_))
-                .WillByDefault(Return(packageInfoMapping));
+                .WillByDefault(Return(kPackageInfosByUid));
         mMockIoOveruseConfigs->injectPackageConfigs({
                 {"system.daemon",
                  {constructPerStateBytes(/*fgBytes=*/80'000, /*bgBytes=*/40'000,
@@ -244,6 +222,24 @@
         });
     }
 
+    std::vector<UidStats> constructUidStats(
+            std::unordered_map<uid_t, std::tuple<int32_t, int32_t>> writtenBytesByUid) {
+        std::vector<UidStats> uidStats;
+        for (const auto& [uid, writtenBytes] : writtenBytesByUid) {
+            PackageInfo packageInfo;
+            if (kPackageInfosByUid.find(uid) != kPackageInfosByUid.end()) {
+                packageInfo = kPackageInfosByUid.at(uid);
+            }
+            uidStats.push_back(UidStats{.packageInfo = packageInfo,
+                                        .ioStats = {/*fgRdBytes=*/989'000,
+                                                    /*bgRdBytes=*/678'000,
+                                                    /*fgWrBytes=*/std::get<0>(writtenBytes),
+                                                    /*bgWrBytes=*/std::get<1>(writtenBytes),
+                                                    /*fgFsync=*/10'000, /*bgFsync=*/50'000}});
+        }
+        return uidStats;
+    }
+
     void executeAsUid(uid_t uid, std::function<void()> func) {
         sp<ScopedChangeCallingUid> scopedChangeCallingUid = sp<ScopedChangeCallingUid>::make(uid);
         ASSERT_NO_FATAL_FAILURE(func());
@@ -252,12 +248,36 @@
     sp<MockWatchdogServiceHelper> mMockWatchdogServiceHelper;
     sp<MockIoOveruseConfigs> mMockIoOveruseConfigs;
     sp<MockPackageInfoResolver> mMockPackageInfoResolver;
+    sp<MockUidStatsCollector> mMockUidStatsCollector;
     sp<IoOveruseMonitor> mIoOveruseMonitor;
     sp<internal::IoOveruseMonitorPeer> mIoOveruseMonitorPeer;
+
+    static const std::unordered_map<uid_t, PackageInfo> kPackageInfosByUid;
 };
 
+const std::unordered_map<uid_t, PackageInfo> IoOveruseMonitorTest::kPackageInfosByUid =
+        {{1001000,
+          constructPackageInfo(
+                  /*packageName=*/"system.daemon",
+                  /*uid=*/1001000, UidType::NATIVE)},
+         {1112345,
+          constructPackageInfo(
+                  /*packageName=*/"com.android.google.package",
+                  /*uid=*/1112345, UidType::APPLICATION)},
+         {1113999,
+          constructPackageInfo(
+                  /*packageName=*/"com.android.google.package",
+                  /*uid=*/1113999, UidType::APPLICATION)},
+         {1212345,
+          constructPackageInfo(
+                  /*packageName=*/"com.android.google.package",
+                  /*uid=*/1212345, UidType::APPLICATION)},
+         {1312345,
+          constructPackageInfo(
+                  /*packageName=*/"com.android.google.package",
+                  /*uid=*/1312345, UidType::APPLICATION)}};
+
 TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollection) {
-    setUpPackagesAndConfigurations();
     sp<MockResourceOveruseListener> mockResourceOveruseListener =
             sp<MockResourceOveruseListener>::make();
     ASSERT_NO_FATAL_FAILURE(executeAsUid(1001000, [&]() {
@@ -268,11 +288,11 @@
      * Package "system.daemon" (UID: 1001000) exceeds warn threshold percentage of 80% but no
      * warning is issued as it is a native UID.
      */
-    sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000, 0, 0)},
-             {1112345, IoUsage(0, 0, /*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000, 0, 0)},
-             {1212345, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000, 0, 0)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(
+                    constructUidStats({{1001000, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}},
+                                       {1112345, {/*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000}},
+                                       {1212345, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}}})));
 
     std::vector<PackageIoOveruseStats> actualIoOveruseStats;
     EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
@@ -282,7 +302,7 @@
     const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
 
     ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
-                                                             mockUidIoStats, nullptr, nullptr));
+                                                             mMockUidStatsCollector, nullptr));
 
     std::vector<PackageIoOveruseStats> expectedIoOveruseStats =
             {constructPackageIoOveruseStats(/*uid*=*/1001000, /*shouldNotify=*/false,
@@ -308,10 +328,11 @@
 
     ResourceOveruseStats actualOverusingNativeStats;
     // Package "com.android.google.package" for user 11 changed uid from 1112345 to 1113999.
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(0, 0, /*fgWrBytes=*/30'000, /*bgWrBytes=*/0, 0, 0)},
-             {1113999, IoUsage(0, 0, /*fgWrBytes=*/25'000, /*bgWrBytes=*/10'000, 0, 0)},
-             {1212345, IoUsage(0, 0, /*fgWrBytes=*/20'000, /*bgWrBytes=*/30'000, 0, 0)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(
+                    constructUidStats({{1001000, {/*fgWrBytes=*/30'000, /*bgWrBytes=*/0}},
+                                       {1113999, {/*fgWrBytes=*/25'000, /*bgWrBytes=*/10'000}},
+                                       {1212345, {/*fgWrBytes=*/20'000, /*bgWrBytes=*/30'000}}})));
     actualIoOveruseStats.clear();
     EXPECT_CALL(*mockResourceOveruseListener, onOveruse(_))
             .WillOnce(DoAll(SaveArg<0>(&actualOverusingNativeStats), Return(Status::ok())));
@@ -319,14 +340,14 @@
             .WillOnce(DoAll(SaveArg<0>(&actualIoOveruseStats), Return(Status::ok())));
 
     ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
-                                                             mockUidIoStats, nullptr, nullptr));
+                                                             mMockUidStatsCollector, nullptr));
 
     const auto expectedOverusingNativeStats = constructResourceOveruseStats(
             constructIoOveruseStats(/*isKillable=*/false,
                                     /*remaining=*/constructPerStateBytes(0, 20'000, 100'000),
                                     /*written=*/constructPerStateBytes(100'000, 20'000, 0),
                                     /*totalOveruses=*/1, startTime, durationInSeconds));
-    EXPECT_THAT(actualOverusingNativeStats, expectedOverusingNativeStats)
+    EXPECT_THAT(actualOverusingNativeStats, Eq(expectedOverusingNativeStats))
             << "Expected: " << expectedOverusingNativeStats.toString()
             << "\nActual: " << actualOverusingNativeStats.toString();
 
@@ -360,17 +381,18 @@
      * Current date changed so the daily I/O usage stats should be reset and the latest I/O overuse
      * stats should not aggregate with the previous day's stats.
      */
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(0, 0, /*fgWrBytes=*/78'000, /*bgWrBytes=*/38'000, 0, 0)},
-             {1113999, IoUsage(0, 0, /*fgWrBytes=*/55'000, /*bgWrBytes=*/23'000, 0, 0)},
-             {1212345, IoUsage(0, 0, /*fgWrBytes=*/55'000, /*bgWrBytes=*/23'000, 0, 0)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(
+                    constructUidStats({{1001000, {/*fgWrBytes=*/78'000, /*bgWrBytes=*/38'000}},
+                                       {1113999, {/*fgWrBytes=*/55'000, /*bgWrBytes=*/23'000}},
+                                       {1212345, {/*fgWrBytes=*/55'000, /*bgWrBytes=*/23'000}}})));
     actualIoOveruseStats.clear();
     EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
             .WillOnce(DoAll(SaveArg<0>(&actualIoOveruseStats), Return(Status::ok())));
 
     currentTime += (24 * 60 * 60);  // Change collection time to next day.
     ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
-                                                             mockUidIoStats, nullptr, nullptr));
+                                                             mMockUidStatsCollector, nullptr));
 
     const auto [nextDayStartTime, nextDayDuration] = calculateStartAndDuration(currentTime);
     expectedIoOveruseStats =
@@ -397,7 +419,6 @@
 }
 
 TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithGarageMode) {
-    setUpPackagesAndConfigurations();
     sp<MockResourceOveruseListener> mockResourceOveruseListener =
             sp<MockResourceOveruseListener>::make();
     ASSERT_NO_FATAL_FAILURE(executeAsUid(1001000, [&]() {
@@ -408,11 +429,11 @@
      * Package "system.daemon" (UID: 1001000) exceeds warn threshold percentage of 80% but no
      * warning is issued as it is a native UID.
      */
-    sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/60'000, 0, 0)},
-             {1112345, IoUsage(0, 0, /*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000, 0, 0)},
-             {1212345, IoUsage(0, 0, /*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000, 0, 0)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(
+                    constructUidStats({{1001000, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/60'000}},
+                                       {1112345, {/*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000}},
+                                       {1212345, {/*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000}}})));
 
     ResourceOveruseStats actualOverusingNativeStats;
     EXPECT_CALL(*mockResourceOveruseListener, onOveruse(_))
@@ -425,14 +446,14 @@
     const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
 
     ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::GARAGE_MODE,
-                                                             mockUidIoStats, nullptr, nullptr));
+                                                             mMockUidStatsCollector, nullptr));
 
     const auto expectedOverusingNativeStats = constructResourceOveruseStats(
             constructIoOveruseStats(/*isKillable=*/false,
                                     /*remaining=*/constructPerStateBytes(80'000, 40'000, 0),
                                     /*written=*/constructPerStateBytes(0, 0, 130'000),
                                     /*totalOveruses=*/1, startTime, durationInSeconds));
-    EXPECT_THAT(actualOverusingNativeStats, expectedOverusingNativeStats)
+    EXPECT_THAT(actualOverusingNativeStats, Eq(expectedOverusingNativeStats))
             << "Expected: " << expectedOverusingNativeStats.toString()
             << "\nActual: " << actualOverusingNativeStats.toString();
 
@@ -460,11 +481,10 @@
 }
 
 TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithZeroWriteBytes) {
-    sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(10, 0, /*fgWrBytes=*/0, /*bgWrBytes=*/0, 1, 0)},
-             {1112345, IoUsage(0, 20, /*fgWrBytes=*/0, /*bgWrBytes=*/0, 0, 0)},
-             {1212345, IoUsage(0, 00, /*fgWrBytes=*/0, /*bgWrBytes=*/0, 0, 1)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(constructUidStats({{1001000, {/*fgWrBytes=*/0, /*bgWrBytes=*/0}},
+                                                {1112345, {/*fgWrBytes=*/0, /*bgWrBytes=*/0}},
+                                                {1212345, {/*fgWrBytes=*/0, /*bgWrBytes=*/0}}})));
 
     EXPECT_CALL(*mMockPackageInfoResolver, getPackageInfosForUids(_)).Times(0);
     EXPECT_CALL(*mMockIoOveruseConfigs, fetchThreshold(_)).Times(0);
@@ -474,22 +494,21 @@
     ASSERT_RESULT_OK(
             mIoOveruseMonitor->onPeriodicCollection(std::chrono::system_clock::to_time_t(
                                                             std::chrono::system_clock::now()),
-                                                    SystemState::NORMAL_MODE, mockUidIoStats,
-                                                    nullptr, nullptr));
+                                                    SystemState::NORMAL_MODE,
+                                                    mMockUidStatsCollector, nullptr));
 }
 
 TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithSmallWrittenBytes) {
-    setUpPackagesAndConfigurations();
-    sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
     /*
      * UID 1212345 current written bytes < |KTestMinSyncWrittenBytes| so the UID's stats are not
      * synced.
      */
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(10, 0, /*fgWrBytes=*/59'200, /*bgWrBytes=*/0, 1, 0)},
-             {1112345, IoUsage(0, 20, /*fgWrBytes=*/0, /*bgWrBytes=*/25'200, 0, 0)},
-             {1212345, IoUsage(0, 00, /*fgWrBytes=*/300, /*bgWrBytes=*/600, 0, 1)},
-             {1312345, IoUsage(0, 00, /*fgWrBytes=*/51'200, /*bgWrBytes=*/0, 0, 1)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(
+                    constructUidStats({{1001000, {/*fgWrBytes=*/59'200, /*bgWrBytes=*/0}},
+                                       {1112345, {/*fgWrBytes=*/0, /*bgWrBytes=*/25'200}},
+                                       {1212345, {/*fgWrBytes=*/300, /*bgWrBytes=*/600}},
+                                       {1312345, {/*fgWrBytes=*/51'200, /*bgWrBytes=*/0}}})));
 
     std::vector<PackageIoOveruseStats> actualIoOveruseStats;
     EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
@@ -499,7 +518,7 @@
     const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
 
     ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
-                                                             mockUidIoStats, nullptr, nullptr));
+                                                             mMockUidStatsCollector, nullptr));
 
     std::vector<PackageIoOveruseStats> expectedIoOveruseStats =
             {constructPackageIoOveruseStats(/*uid*=*/1001000, /*shouldNotify=*/false,
@@ -537,19 +556,15 @@
      * UID 1312345 current written bytes is < |kTestMinSyncWrittenBytes| but exceeds warn threshold
      * and killable so the UID's stat are synced.
      */
-    mockUidIoStats->expectDeltaStats(
-            {{1001000,
-              IoUsage(10, 0, /*fgWrBytes=*/KTestMinSyncWrittenBytes - 100, /*bgWrBytes=*/0, 1, 0)},
-             {1112345,
-              IoUsage(0, 20, /*fgWrBytes=*/0, /*bgWrBytes=*/KTestMinSyncWrittenBytes - 100, 0, 0)},
-             {1212345,
-              IoUsage(0, 00, /*fgWrBytes=*/KTestMinSyncWrittenBytes - 300, /*bgWrBytes=*/0, 0, 1)},
-             {1312345,
-              IoUsage(0, 00, /*fgWrBytes=*/KTestMinSyncWrittenBytes - 100, /*bgWrBytes=*/0, 0,
-                      1)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(constructUidStats(
+                    {{1001000, {/*fgWrBytes=*/KTestMinSyncWrittenBytes - 100, /*bgWrBytes=*/0}},
+                     {1112345, {/*fgWrBytes=*/0, /*bgWrBytes=*/KTestMinSyncWrittenBytes - 100}},
+                     {1212345, {/*fgWrBytes=*/KTestMinSyncWrittenBytes - 300, /*bgWrBytes=*/0}},
+                     {1312345, {/*fgWrBytes=*/KTestMinSyncWrittenBytes - 100, /*bgWrBytes=*/0}}})));
 
     ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
-                                                             mockUidIoStats, nullptr, nullptr));
+                                                             mMockUidStatsCollector, nullptr));
 
     expectedIoOveruseStats =
             {constructPackageIoOveruseStats(/*uid*=*/1112345, /*shouldNotify=*/true,
@@ -573,14 +588,11 @@
 }
 
 TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithNoPackageInfo) {
-    sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000, 0, 0)},
-             {1112345, IoUsage(0, 0, /*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000, 0, 0)},
-             {1212345, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000, 0, 0)}});
-
-    ON_CALL(*mMockPackageInfoResolver, getPackageInfosForUids(_))
-            .WillByDefault(Return(std::unordered_map<uid_t, PackageInfo>{}));
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(
+                    constructUidStats({{2301000, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}},
+                                       {2412345, {/*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000}},
+                                       {2512345, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}}})));
 
     EXPECT_CALL(*mMockIoOveruseConfigs, fetchThreshold(_)).Times(0);
     EXPECT_CALL(*mMockIoOveruseConfigs, isSafeToKill(_)).Times(0);
@@ -589,8 +601,8 @@
     ASSERT_RESULT_OK(
             mIoOveruseMonitor->onPeriodicCollection(std::chrono::system_clock::to_time_t(
                                                             std::chrono::system_clock::now()),
-                                                    SystemState::NORMAL_MODE, mockUidIoStats,
-                                                    nullptr, nullptr));
+                                                    SystemState::NORMAL_MODE,
+                                                    mMockUidStatsCollector, nullptr));
 }
 
 TEST_F(IoOveruseMonitorTest, TestOnPeriodicMonitor) {
@@ -712,16 +724,15 @@
 }
 
 TEST_F(IoOveruseMonitorTest, TestGetIoOveruseStats) {
-    setUpPackagesAndConfigurations();
-    sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(0, 0, /*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000, 0, 0)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(
+                    constructUidStats({{1001000, {/*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000}}})));
 
     time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
     const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
 
     ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
-                                                             mockUidIoStats, nullptr, nullptr));
+                                                             mMockUidStatsCollector, nullptr));
 
     const auto expected =
             constructIoOveruseStats(/*isKillable=*/false,
@@ -739,16 +750,15 @@
 }
 
 TEST_F(IoOveruseMonitorTest, TestResetIoOveruseStats) {
-    setUpPackagesAndConfigurations();
-    sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
-    mockUidIoStats->expectDeltaStats(
-            {{1001000, IoUsage(0, 0, /*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000, 0, 0)}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+            .WillOnce(Return(
+                    constructUidStats({{1001000, {/*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000}}})));
 
     ASSERT_RESULT_OK(
             mIoOveruseMonitor->onPeriodicCollection(std::chrono::system_clock::to_time_t(
                                                             std::chrono::system_clock::now()),
-                                                    SystemState::NORMAL_MODE, mockUidIoStats,
-                                                    nullptr, nullptr));
+                                                    SystemState::NORMAL_MODE,
+                                                    mMockUidStatsCollector, nullptr));
 
     IoOveruseStats actual;
     ASSERT_NO_FATAL_FAILURE(executeAsUid(1001000, [&]() {
diff --git a/cpp/watchdog/server/tests/IoPerfCollectionTest.cpp b/cpp/watchdog/server/tests/IoPerfCollectionTest.cpp
index 85e049d..8da3b6f 100644
--- a/cpp/watchdog/server/tests/IoPerfCollectionTest.cpp
+++ b/cpp/watchdog/server/tests/IoPerfCollectionTest.cpp
@@ -15,101 +15,182 @@
  */
 
 #include "IoPerfCollection.h"
-#include "MockPackageInfoResolver.h"
-#include "MockProcPidStat.h"
 #include "MockProcStat.h"
-#include "MockUidIoStats.h"
+#include "MockUidStatsCollector.h"
 #include "MockWatchdogServiceHelper.h"
-#include "PackageInfoResolver.h"
+#include "PackageInfoTestUtils.h"
 
 #include <WatchdogProperties.sysprop.h>
 #include <android-base/file.h>
 #include <gmock/gmock.h>
+#include <utils/RefBase.h>
 
 #include <sys/types.h>
 #include <unistd.h>
 
 #include <string>
+#include <type_traits>
 #include <vector>
 
 namespace android {
 namespace automotive {
 namespace watchdog {
 
+using ::android::RefBase;
 using ::android::sp;
-using ::android::base::Error;
+using ::android::automotive::watchdog::internal::PackageInfo;
 using ::android::base::ReadFdToString;
 using ::android::base::Result;
 using ::testing::_;
+using ::testing::AllOf;
+using ::testing::ElementsAreArray;
+using ::testing::Eq;
+using ::testing::ExplainMatchResult;
+using ::testing::Field;
+using ::testing::IsSubsetOf;
+using ::testing::Matcher;
 using ::testing::Return;
+using ::testing::Test;
+using ::testing::UnorderedElementsAreArray;
+using ::testing::VariantWith;
 
 namespace {
 
-bool isEqual(const UidIoPerfData& lhs, const UidIoPerfData& rhs) {
-    if (lhs.topNReads.size() != rhs.topNReads.size() ||
-        lhs.topNWrites.size() != rhs.topNWrites.size()) {
-        return false;
+MATCHER_P(IoStatsEq, expected, "") {
+    return ExplainMatchResult(AllOf(Field("bytes", &UserPackageStats::IoStats::bytes,
+                                          ElementsAreArray(expected.bytes)),
+                                    Field("fsync", &UserPackageStats::IoStats::fsync,
+                                          ElementsAreArray(expected.fsync))),
+                              arg, result_listener);
+}
+
+MATCHER_P(ProcessCountEq, expected, "") {
+    return ExplainMatchResult(AllOf(Field("comm", &UserPackageStats::ProcStats::ProcessCount::comm,
+                                          Eq(expected.comm)),
+                                    Field("count",
+                                          &UserPackageStats::ProcStats::ProcessCount::count,
+                                          Eq(expected.count))),
+                              arg, result_listener);
+}
+
+MATCHER_P(ProcStatsEq, expected, "") {
+    std::vector<Matcher<const UserPackageStats::ProcStats::ProcessCount&>> processCountMatchers;
+    for (const auto& processCount : expected.topNProcesses) {
+        processCountMatchers.push_back(ProcessCountEq(processCount));
     }
-    for (int i = 0; i < METRIC_TYPES; ++i) {
-        for (int j = 0; j < UID_STATES; ++j) {
-            if (lhs.total[i][j] != rhs.total[i][j]) {
+    return ExplainMatchResult(AllOf(Field("count", &UserPackageStats::ProcStats::count,
+                                          Eq(expected.count)),
+                                    Field("topNProcesses",
+                                          &UserPackageStats::ProcStats::topNProcesses,
+                                          ElementsAreArray(processCountMatchers))),
+                              arg, result_listener);
+}
+
+MATCHER_P(UserPackageStatsEq, expected, "") {
+    const auto uidMatcher = Field("uid", &UserPackageStats::uid, Eq(expected.uid));
+    const auto packageNameMatcher =
+            Field("genericPackageName", &UserPackageStats::genericPackageName,
+                  Eq(expected.genericPackageName));
+    return std::visit(
+            [&](const auto& stats) -> bool {
+                using T = std::decay_t<decltype(stats)>;
+                if constexpr (std::is_same_v<T, UserPackageStats::IoStats>) {
+                    return ExplainMatchResult(AllOf(uidMatcher, packageNameMatcher,
+                                                    Field("stats:IoStats", &UserPackageStats::stats,
+                                                          VariantWith<UserPackageStats::IoStats>(
+                                                                  IoStatsEq(stats)))),
+                                              arg, result_listener);
+                } else if constexpr (std::is_same_v<T, UserPackageStats::ProcStats>) {
+                    return ExplainMatchResult(AllOf(uidMatcher, packageNameMatcher,
+                                                    Field("stats:ProcStats",
+                                                          &UserPackageStats::stats,
+                                                          VariantWith<UserPackageStats::ProcStats>(
+                                                                  ProcStatsEq(stats)))),
+                                              arg, result_listener);
+                }
+                *result_listener << "Unexpected variant in UserPackageStats::stats";
                 return false;
-            }
+            },
+            expected.stats);
+}
+
+MATCHER_P(UserPackageSummaryStatsEq, expected, "") {
+    const auto& userPackageStatsMatchers = [&](const std::vector<UserPackageStats>& stats) {
+        std::vector<Matcher<const UserPackageStats&>> matchers;
+        for (const auto& curStats : stats) {
+            matchers.push_back(UserPackageStatsEq(curStats));
         }
-    }
-    auto comp = [&](const UidIoPerfData::Stats& l, const UidIoPerfData::Stats& r) -> bool {
-        bool isEqual = l.userId == r.userId && l.packageName == r.packageName;
-        for (int i = 0; i < UID_STATES; ++i) {
-            isEqual &= l.bytes[i] == r.bytes[i] && l.fsync[i] == r.fsync[i];
+        return ElementsAreArray(matchers);
+    };
+    const auto& totalIoStatsArrayMatcher = [&](const int64_t expected[][UID_STATES]) {
+        std::vector<Matcher<const int64_t[UID_STATES]>> matchers;
+        for (int i = 0; i < METRIC_TYPES; ++i) {
+            matchers.push_back(ElementsAreArray(expected[i], UID_STATES));
         }
-        return isEqual;
+        return ElementsAreArray(matchers);
     };
-    return lhs.topNReads.size() == rhs.topNReads.size() &&
-            std::equal(lhs.topNReads.begin(), lhs.topNReads.end(), rhs.topNReads.begin(), comp) &&
-            lhs.topNWrites.size() == rhs.topNWrites.size() &&
-            std::equal(lhs.topNWrites.begin(), lhs.topNWrites.end(), rhs.topNWrites.begin(), comp);
+    return ExplainMatchResult(AllOf(Field("topNIoReads", &UserPackageSummaryStats::topNIoReads,
+                                          userPackageStatsMatchers(expected.topNIoReads)),
+                                    Field("topNIoWrites", &UserPackageSummaryStats::topNIoWrites,
+                                          userPackageStatsMatchers(expected.topNIoWrites)),
+                                    Field("topNIoBlocked", &UserPackageSummaryStats::topNIoBlocked,
+                                          userPackageStatsMatchers(expected.topNIoBlocked)),
+                                    Field("topNMajorFaults",
+                                          &UserPackageSummaryStats::topNMajorFaults,
+                                          userPackageStatsMatchers(expected.topNMajorFaults)),
+                                    Field("totalIoStats", &UserPackageSummaryStats::totalIoStats,
+                                          totalIoStatsArrayMatcher(expected.totalIoStats)),
+                                    Field("taskCountByUid",
+                                          &UserPackageSummaryStats::taskCountByUid,
+                                          IsSubsetOf(expected.taskCountByUid)),
+                                    Field("totalMajorFaults",
+                                          &UserPackageSummaryStats::totalMajorFaults,
+                                          Eq(expected.totalMajorFaults)),
+                                    Field("majorFaultsPercentChange",
+                                          &UserPackageSummaryStats::majorFaultsPercentChange,
+                                          Eq(expected.majorFaultsPercentChange))),
+                              arg, result_listener);
 }
 
-bool isEqual(const SystemIoPerfData& lhs, const SystemIoPerfData& rhs) {
-    return lhs.cpuIoWaitTime == rhs.cpuIoWaitTime && lhs.totalCpuTime == rhs.totalCpuTime &&
-            lhs.ioBlockedProcessesCnt == rhs.ioBlockedProcessesCnt &&
-            lhs.totalProcessesCnt == rhs.totalProcessesCnt;
+MATCHER_P(SystemSummaryStatsEq, expected, "") {
+    return ExplainMatchResult(AllOf(Field("cpuIoWaitTime", &SystemSummaryStats::cpuIoWaitTime,
+                                          Eq(expected.cpuIoWaitTime)),
+                                    Field("totalCpuTime", &SystemSummaryStats::totalCpuTime,
+                                          Eq(expected.totalCpuTime)),
+                                    Field("ioBlockedProcessCount",
+                                          &SystemSummaryStats::ioBlockedProcessCount,
+                                          Eq(expected.ioBlockedProcessCount)),
+                                    Field("totalProcessCount",
+                                          &SystemSummaryStats::totalProcessCount,
+                                          Eq(expected.totalProcessCount))),
+                              arg, result_listener);
 }
 
-bool isEqual(const ProcessIoPerfData& lhs, const ProcessIoPerfData& rhs) {
-    if (lhs.topNIoBlockedUids.size() != rhs.topNIoBlockedUids.size() ||
-        lhs.topNMajorFaultUids.size() != rhs.topNMajorFaultUids.size() ||
-        lhs.totalMajorFaults != rhs.totalMajorFaults ||
-        lhs.majorFaultsPercentChange != rhs.majorFaultsPercentChange) {
-        return false;
+MATCHER_P(PerfStatsRecordEq, expected, "") {
+    return ExplainMatchResult(AllOf(Field(&PerfStatsRecord::systemSummaryStats,
+                                          SystemSummaryStatsEq(expected.systemSummaryStats)),
+                                    Field(&PerfStatsRecord::userPackageSummaryStats,
+                                          UserPackageSummaryStatsEq(
+                                                  expected.userPackageSummaryStats))),
+                              arg, result_listener);
+}
+
+const std::vector<Matcher<const PerfStatsRecord&>> constructPerfStatsRecordMatchers(
+        const std::vector<PerfStatsRecord>& records) {
+    std::vector<Matcher<const PerfStatsRecord&>> matchers;
+    for (const auto& record : records) {
+        matchers.push_back(PerfStatsRecordEq(record));
     }
-    auto comp = [&](const ProcessIoPerfData::UidStats& l,
-                    const ProcessIoPerfData::UidStats& r) -> bool {
-        auto comp = [&](const ProcessIoPerfData::UidStats::ProcessStats& l,
-                        const ProcessIoPerfData::UidStats::ProcessStats& r) -> bool {
-            return l.comm == r.comm && l.count == r.count;
-        };
-        return l.userId == r.userId && l.packageName == r.packageName && l.count == r.count &&
-                l.topNProcesses.size() == r.topNProcesses.size() &&
-                std::equal(l.topNProcesses.begin(), l.topNProcesses.end(), r.topNProcesses.begin(),
-                           comp);
-    };
-    return lhs.topNIoBlockedUids.size() == lhs.topNIoBlockedUids.size() &&
-            std::equal(lhs.topNIoBlockedUids.begin(), lhs.topNIoBlockedUids.end(),
-                       rhs.topNIoBlockedUids.begin(), comp) &&
-            lhs.topNIoBlockedUidsTotalTaskCnt.size() == rhs.topNIoBlockedUidsTotalTaskCnt.size() &&
-            std::equal(lhs.topNIoBlockedUidsTotalTaskCnt.begin(),
-                       lhs.topNIoBlockedUidsTotalTaskCnt.end(),
-                       rhs.topNIoBlockedUidsTotalTaskCnt.begin()) &&
-            lhs.topNMajorFaultUids.size() == rhs.topNMajorFaultUids.size() &&
-            std::equal(lhs.topNMajorFaultUids.begin(), lhs.topNMajorFaultUids.end(),
-                       rhs.topNMajorFaultUids.begin(), comp);
+    return matchers;
 }
 
-bool isEqual(const IoPerfRecord& lhs, const IoPerfRecord& rhs) {
-    return isEqual(lhs.uidIoPerfData, rhs.uidIoPerfData) &&
-            isEqual(lhs.systemIoPerfData, rhs.systemIoPerfData) &&
-            isEqual(lhs.processIoPerfData, rhs.processIoPerfData);
+MATCHER_P(CollectionInfoEq, expected, "") {
+    return ExplainMatchResult(AllOf(Field("maxCacheSize", &CollectionInfo::maxCacheSize,
+                                          Eq(expected.maxCacheSize)),
+                                    Field("records", &CollectionInfo::records,
+                                          ElementsAreArray(constructPerfStatsRecordMatchers(
+                                                  expected.records)))),
+                              arg, result_listener);
 }
 
 int countOccurrences(std::string str, std::string subStr) {
@@ -122,23 +203,167 @@
     return occurrences;
 }
 
+std::tuple<std::vector<UidStats>, UserPackageSummaryStats> sampleUidStats(int multiplier = 1) {
+    /* The number of returned sample stats are less that the top N stats per category/sub-category.
+     * The top N stats per category/sub-category is set to % during test setup. Thus, the default
+     * testing behavior is # reported stats < top N stats.
+     */
+    const auto int64Multiplier = [&](int64_t bytes) -> int64_t {
+        return static_cast<int64_t>(bytes * multiplier);
+    };
+    const auto uint64Multiplier = [&](uint64_t count) -> uint64_t {
+        return static_cast<uint64_t>(count * multiplier);
+    };
+    std::vector<UidStats>
+            uidStats{{.packageInfo = constructPackageInfo("mount", 1009),
+                      .ioStats = {/*fgRdBytes=*/0,
+                                  /*bgRdBytes=*/int64Multiplier(14'000),
+                                  /*fgWrBytes=*/0,
+                                  /*bgWrBytes=*/int64Multiplier(16'000),
+                                  /*fgFsync=*/0, /*bgFsync=*/int64Multiplier(100)},
+                      .procStats = {.totalMajorFaults = uint64Multiplier(11'000),
+                                    .totalTasksCount = 1,
+                                    .ioBlockedTasksCount = 1,
+                                    .processStatsByPid =
+                                            {{/*pid=*/100,
+                                              {/*comm=*/"disk I/O", /*startTime=*/234,
+                                               /*totalMajorFaults=*/uint64Multiplier(11'000),
+                                               /*totalTasksCount=*/1,
+                                               /*ioBlockedTasksCount=*/1}}}}},
+                     {.packageInfo =
+                              constructPackageInfo("com.google.android.car.kitchensink", 1002001),
+                      .ioStats = {/*fgRdBytes=*/0,
+                                  /*bgRdBytes=*/int64Multiplier(3'400),
+                                  /*fgWrBytes=*/0,
+                                  /*bgWrBytes=*/int64Multiplier(6'700),
+                                  /*fgFsync=*/0,
+                                  /*bgFsync=*/int64Multiplier(200)},
+                      .procStats = {.totalMajorFaults = uint64Multiplier(22'445),
+                                    .totalTasksCount = 5,
+                                    .ioBlockedTasksCount = 3,
+                                    .processStatsByPid =
+                                            {{/*pid=*/1000,
+                                              {/*comm=*/"KitchenSinkApp", /*startTime=*/467,
+                                               /*totalMajorFaults=*/uint64Multiplier(12'345),
+                                               /*totalTasksCount=*/2,
+                                               /*ioBlockedTasksCount=*/1}},
+                                             {/*pid=*/1001,
+                                              {/*comm=*/"CTS", /*startTime=*/789,
+                                               /*totalMajorFaults=*/uint64Multiplier(10'100),
+                                               /*totalTasksCount=*/3,
+                                               /*ioBlockedTasksCount=*/2}}}}},
+                     {.packageInfo = constructPackageInfo("", 1012345),
+                      .ioStats = {/*fgRdBytes=*/int64Multiplier(1'000),
+                                  /*bgRdBytes=*/int64Multiplier(4'200),
+                                  /*fgWrBytes=*/int64Multiplier(300),
+                                  /*bgWrBytes=*/int64Multiplier(5'600),
+                                  /*fgFsync=*/int64Multiplier(600),
+                                  /*bgFsync=*/int64Multiplier(300)},
+                      .procStats = {.totalMajorFaults = uint64Multiplier(50'900),
+                                    .totalTasksCount = 4,
+                                    .ioBlockedTasksCount = 2,
+                                    .processStatsByPid =
+                                            {{/*pid=*/2345,
+                                              {/*comm=*/"MapsApp", /*startTime=*/6789,
+                                               /*totalMajorFaults=*/uint64Multiplier(50'900),
+                                               /*totalTasksCount=*/4,
+                                               /*ioBlockedTasksCount=*/2}}}}},
+                     {.packageInfo = constructPackageInfo("com.google.radio", 1015678),
+                      .ioStats = {/*fgRdBytes=*/0,
+                                  /*bgRdBytes=*/0,
+                                  /*fgWrBytes=*/0,
+                                  /*bgWrBytes=*/0,
+                                  /*fgFsync=*/0, /*bgFsync=*/0},
+                      .procStats = {.totalMajorFaults = 0,
+                                    .totalTasksCount = 4,
+                                    .ioBlockedTasksCount = 0,
+                                    .processStatsByPid = {
+                                            {/*pid=*/2345,
+                                             {/*comm=*/"RadioApp", /*startTime=*/19789,
+                                              /*totalMajorFaults=*/0,
+                                              /*totalTasksCount=*/4,
+                                              /*ioBlockedTasksCount=*/0}}}}}};
+
+    UserPackageSummaryStats userPackageSummaryStats{
+            .topNIoReads =
+                    {{1009, "mount",
+                      UserPackageStats::IoStats{{0, int64Multiplier(14'000)},
+                                                {0, int64Multiplier(100)}}},
+                     {1012345, "1012345",
+                      UserPackageStats::IoStats{{int64Multiplier(1'000), int64Multiplier(4'200)},
+                                                {int64Multiplier(600), int64Multiplier(300)}}},
+                     {1002001, "com.google.android.car.kitchensink",
+                      UserPackageStats::IoStats{{0, int64Multiplier(3'400)},
+                                                {0, int64Multiplier(200)}}}},
+            .topNIoWrites =
+                    {{1009, "mount",
+                      UserPackageStats::IoStats{{0, int64Multiplier(16'000)},
+                                                {0, int64Multiplier(100)}}},
+                     {1002001, "com.google.android.car.kitchensink",
+                      UserPackageStats::IoStats{{0, int64Multiplier(6'700)},
+                                                {0, int64Multiplier(200)}}},
+                     {1012345, "1012345",
+                      UserPackageStats::IoStats{{int64Multiplier(300), int64Multiplier(5'600)},
+                                                {int64Multiplier(600), int64Multiplier(300)}}}},
+            .topNIoBlocked = {{1002001, "com.google.android.car.kitchensink",
+                               UserPackageStats::ProcStats{3, {{"CTS", 2}, {"KitchenSinkApp", 1}}}},
+                              {1012345, "1012345",
+                               UserPackageStats::ProcStats{2, {{"MapsApp", 2}}}},
+                              {1009, "mount", UserPackageStats::ProcStats{1, {{"disk I/O", 1}}}}},
+            .topNMajorFaults =
+                    {{1012345, "1012345",
+                      UserPackageStats::ProcStats{uint64Multiplier(50'900),
+                                                  {{"MapsApp", uint64Multiplier(50'900)}}}},
+                     {1002001, "com.google.android.car.kitchensink",
+                      UserPackageStats::ProcStats{uint64Multiplier(22'445),
+                                                  {{"KitchenSinkApp", uint64Multiplier(12'345)},
+                                                   {"CTS", uint64Multiplier(10'100)}}}},
+                     {1009, "mount",
+                      UserPackageStats::ProcStats{uint64Multiplier(11'000),
+                                                  {{"disk I/O", uint64Multiplier(11'000)}}}}},
+            .totalIoStats = {{int64Multiplier(1'000), int64Multiplier(21'600)},
+                             {int64Multiplier(300), int64Multiplier(28'300)},
+                             {int64Multiplier(600), int64Multiplier(600)}},
+            .taskCountByUid = {{1009, 1}, {1002001, 5}, {1012345, 4}},
+            .totalMajorFaults = uint64Multiplier(84'345),
+            .majorFaultsPercentChange = 0.0,
+    };
+    return std::make_tuple(uidStats, userPackageSummaryStats);
+}
+
+std::tuple<ProcStatInfo, SystemSummaryStats> sampleProcStat(int multiplier = 1) {
+    const auto uint64Multiplier = [&](uint64_t bytes) -> uint64_t {
+        return static_cast<uint64_t>(bytes * multiplier);
+    };
+    const auto uint32Multiplier = [&](uint32_t bytes) -> uint32_t {
+        return static_cast<uint32_t>(bytes * multiplier);
+    };
+    ProcStatInfo procStatInfo{/*cpuStats=*/{uint64Multiplier(2'900), uint64Multiplier(7'900),
+                                            uint64Multiplier(4'900), uint64Multiplier(8'900),
+                                            /*ioWaitTime=*/uint64Multiplier(5'900),
+                                            uint64Multiplier(6'966), uint64Multiplier(7'980), 0, 0,
+                                            uint64Multiplier(2'930)},
+                              /*runnableProcessCount=*/uint32Multiplier(100),
+                              /*ioBlockedProcessCount=*/uint32Multiplier(57)};
+    SystemSummaryStats systemSummaryStats{/*cpuIoWaitTime=*/uint64Multiplier(5'900),
+                                          /*totalCpuTime=*/uint64Multiplier(48'376),
+                                          /*ioBlockedProcessCount=*/uint32Multiplier(57),
+                                          /*totalProcessCount=*/uint32Multiplier(157)};
+    return std::make_tuple(procStatInfo, systemSummaryStats);
+}
+
 }  // namespace
 
 namespace internal {
 
-class IoPerfCollectionPeer {
+class IoPerfCollectionPeer : public RefBase {
 public:
-    explicit IoPerfCollectionPeer(sp<IoPerfCollection> collector) :
-          mCollector(collector),
-          mMockPackageInfoResolver(new MockPackageInfoResolver()) {
-        mCollector->mPackageInfoResolver = mMockPackageInfoResolver;
-    }
+    explicit IoPerfCollectionPeer(sp<IoPerfCollection> collector) : mCollector(collector) {}
 
     IoPerfCollectionPeer() = delete;
     ~IoPerfCollectionPeer() {
         mCollector->terminate();
         mCollector.clear();
-        mMockPackageInfoResolver.clear();
     }
 
     Result<void> init() { return mCollector->init(); }
@@ -147,11 +372,6 @@
 
     void setTopNStatsPerSubcategory(int value) { mCollector->mTopNStatsPerSubcategory = value; }
 
-    void injectUidToPackageNameMapping(std::unordered_map<uid_t, std::string> mapping) {
-        EXPECT_CALL(*mMockPackageInfoResolver, getPackageNamesForUids(_))
-                .WillRepeatedly(Return(mapping));
-    }
-
     const CollectionInfo& getBoottimeCollectionInfo() {
         Mutex::Autolock lock(mCollector->mMutex);
         return mCollector->mBoottimeCollection;
@@ -169,548 +389,319 @@
 
 private:
     sp<IoPerfCollection> mCollector;
-    sp<MockPackageInfoResolver> mMockPackageInfoResolver;
 };
 
 }  // namespace internal
 
-TEST(IoPerfCollectionTest, TestBoottimeCollection) {
-    sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
-    sp<MockProcStat> mockProcStat = new MockProcStat();
-    sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
+class IoPerfCollectionTest : public Test {
+protected:
+    void SetUp() override {
+        mMockUidStatsCollector = sp<MockUidStatsCollector>::make();
+        mMockProcStat = sp<MockProcStat>::make();
+        mCollector = sp<IoPerfCollection>::make();
+        mCollectorPeer = sp<internal::IoPerfCollectionPeer>::make(mCollector);
+        ASSERT_RESULT_OK(mCollectorPeer->init());
+        mCollectorPeer->setTopNStatsPerCategory(5);
+        mCollectorPeer->setTopNStatsPerSubcategory(5);
+    }
 
-    sp<IoPerfCollection> collector = new IoPerfCollection();
-    internal::IoPerfCollectionPeer collectorPeer(collector);
+    void TearDown() override {
+        mMockUidStatsCollector.clear();
+        mMockProcStat.clear();
+        mCollector.clear();
+        mCollectorPeer.clear();
+    }
 
-    ASSERT_RESULT_OK(collectorPeer.init());
+    void checkDumpContents(int wantedEmptyCollectionInstances) {
+        TemporaryFile dump;
+        ASSERT_RESULT_OK(mCollector->onDump(dump.fd));
 
-    const std::unordered_map<uid_t, UidIoUsage> uidIoUsages({
-            {1009, {.uid = 1009, .ios = {0, 14000, 0, 16000, 0, 100}}},
-    });
-    const ProcStatInfo procStatInfo{
-            /*stats=*/{2900, 7900, 4900, 8900, /*ioWaitTime=*/5900, 6966, 7980, 0, 0, 2930},
-            /*runnableCnt=*/100,
-            /*ioBlockedCnt=*/57,
-    };
-    const std::vector<ProcessStats> processStats({
-            {.tgid = 100,
-             .uid = 1009,
-             .process = {100, "disk I/O", "D", 1, 11000, 1, 234},
-             .threads = {{100, {100, "mount", "D", 1, 11000, 1, 234}}}},
-    });
+        checkDumpFd(wantedEmptyCollectionInstances, dump.fd);
+    }
 
-    EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
-    EXPECT_CALL(*mockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
-    EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(processStats));
+    void checkCustomDumpContents() {
+        TemporaryFile dump;
+        ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(dump.fd));
 
-    const IoPerfRecord expected = {
-            .uidIoPerfData = {.topNReads = {{0, "mount", {0, 14000}, {0, 100}}},
-                              .topNWrites = {{0, "mount", {0, 16000}, {0, 100}}},
-                              .total = {{0, 14000}, {0, 16000}, {0, 100}}},
-            .systemIoPerfData = {5900, 48376, 57, 157},
-            .processIoPerfData =
-                    {.topNIoBlockedUids = {{0, "mount", 1, {{"disk I/O", 1}}}},
-                     .topNIoBlockedUidsTotalTaskCnt = {1},
-                     .topNMajorFaultUids = {{0, "mount", 11000, {{"disk I/O", 11000}}}},
-                     .totalMajorFaults = 11000,
-                     .majorFaultsPercentChange = 0},
-    };
-    collectorPeer.injectUidToPackageNameMapping({{1009, "mount"}});
+        checkDumpFd(/*wantedEmptyCollectionInstances=*/0, dump.fd);
+    }
+
+private:
+    void checkDumpFd(int wantedEmptyCollectionInstances, int fd) {
+        lseek(fd, 0, SEEK_SET);
+        std::string dumpContents;
+        ASSERT_TRUE(ReadFdToString(fd, &dumpContents));
+        ASSERT_FALSE(dumpContents.empty());
+
+        ASSERT_EQ(countOccurrences(dumpContents, kEmptyCollectionMessage),
+                  wantedEmptyCollectionInstances)
+                << "Dump contents: " << dumpContents;
+    }
+
+protected:
+    sp<MockUidStatsCollector> mMockUidStatsCollector;
+    sp<MockProcStat> mMockProcStat;
+    sp<IoPerfCollection> mCollector;
+    sp<internal::IoPerfCollectionPeer> mCollectorPeer;
+};
+
+TEST_F(IoPerfCollectionTest, TestOnBoottimeCollection) {
+    const auto [uidStats, userPackageSummaryStats] = sampleUidStats();
+    const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
+
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+    EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
 
     time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
-    ASSERT_RESULT_OK(
-            collector->onBoottimeCollection(now, mockUidIoStats, mockProcStat, mockProcPidStat));
+    ASSERT_RESULT_OK(mCollector->onBoottimeCollection(now, mMockUidStatsCollector, mMockProcStat));
 
-    const CollectionInfo& collectionInfo = collectorPeer.getBoottimeCollectionInfo();
+    const auto actual = mCollectorPeer->getBoottimeCollectionInfo();
 
-    ASSERT_EQ(collectionInfo.maxCacheSize, std::numeric_limits<std::size_t>::max());
-    ASSERT_EQ(collectionInfo.records.size(), 1);
-    ASSERT_TRUE(isEqual(collectionInfo.records[0], expected))
-            << "Boottime collection record doesn't match.\nExpected:\n"
-            << toString(expected) << "\nActual:\n"
-            << toString(collectionInfo.records[0]);
+    const CollectionInfo expected{
+            .maxCacheSize = std::numeric_limits<std::size_t>::max(),
+            .records = {{
+                    .systemSummaryStats = systemSummaryStats,
+                    .userPackageSummaryStats = userPackageSummaryStats,
+            }},
+    };
 
-    TemporaryFile dump;
-    ASSERT_RESULT_OK(collector->onDump(dump.fd));
+    EXPECT_THAT(actual, CollectionInfoEq(expected))
+            << "Boottime collection info doesn't match.\nExpected:\n"
+            << expected.toString() << "\nActual:\n"
+            << actual.toString();
 
-    lseek(dump.fd, 0, SEEK_SET);
-    std::string dumpContents;
-    ASSERT_TRUE(ReadFdToString(dump.fd, &dumpContents));
-    ASSERT_FALSE(dumpContents.empty());
-
-    ASSERT_EQ(countOccurrences(dumpContents, kEmptyCollectionMessage), 1)
-            << "Only periodic collection should be not collected. Dump contents: " << dumpContents;
+    ASSERT_NO_FATAL_FAILURE(checkDumpContents(/*wantedEmptyCollectionInstances=*/1))
+            << "Periodic collection shouldn't be reported";
 }
 
-TEST(IoPerfCollectionTest, TestPeriodicCollection) {
-    sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
-    sp<MockProcStat> mockProcStat = new MockProcStat();
-    sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
+TEST_F(IoPerfCollectionTest, TestOnPeriodicCollection) {
+    const auto [uidStats, userPackageSummaryStats] = sampleUidStats();
+    const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
 
-    sp<IoPerfCollection> collector = new IoPerfCollection();
-    internal::IoPerfCollectionPeer collectorPeer(collector);
-
-    ASSERT_RESULT_OK(collectorPeer.init());
-
-    const std::unordered_map<uid_t, UidIoUsage> uidIoUsages({
-            {1009, {.uid = 1009, .ios = {0, 14000, 0, 16000, 0, 100}}},
-    });
-    const ProcStatInfo procStatInfo{
-            /*stats=*/{2900, 7900, 4900, 8900, /*ioWaitTime=*/5900, 6966, 7980, 0, 0, 2930},
-            /*runnableCnt=*/100,
-            /*ioBlockedCnt=*/57,
-    };
-    const std::vector<ProcessStats> processStats({
-            {.tgid = 100,
-             .uid = 1009,
-             .process = {100, "disk I/O", "D", 1, 11000, 1, 234},
-             .threads = {{100, {100, "mount", "D", 1, 11000, 1, 234}}}},
-    });
-
-    EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
-    EXPECT_CALL(*mockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
-    EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(processStats));
-
-    const IoPerfRecord expected = {
-            .uidIoPerfData = {.topNReads = {{0, "mount", {0, 14000}, {0, 100}}},
-                              .topNWrites = {{0, "mount", {0, 16000}, {0, 100}}},
-                              .total = {{0, 14000}, {0, 16000}, {0, 100}}},
-            .systemIoPerfData = {5900, 48376, 57, 157},
-            .processIoPerfData =
-                    {.topNIoBlockedUids = {{0, "mount", 1, {{"disk I/O", 1}}}},
-                     .topNIoBlockedUidsTotalTaskCnt = {1},
-                     .topNMajorFaultUids = {{0, "mount", 11000, {{"disk I/O", 11000}}}},
-                     .totalMajorFaults = 11000,
-                     .majorFaultsPercentChange = 0},
-    };
-
-    collectorPeer.injectUidToPackageNameMapping({{1009, "mount"}});
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+    EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
 
     time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
-    ASSERT_RESULT_OK(collector->onPeriodicCollection(now, SystemState::NORMAL_MODE, mockUidIoStats,
-                                                     mockProcStat, mockProcPidStat));
+    ASSERT_RESULT_OK(mCollector->onPeriodicCollection(now, SystemState::NORMAL_MODE,
+                                                      mMockUidStatsCollector, mMockProcStat));
 
-    const CollectionInfo& collectionInfo = collectorPeer.getPeriodicCollectionInfo();
+    const auto actual = mCollectorPeer->getPeriodicCollectionInfo();
 
-    ASSERT_EQ(collectionInfo.maxCacheSize,
-              static_cast<size_t>(sysprop::periodicCollectionBufferSize().value_or(
-                      kDefaultPeriodicCollectionBufferSize)));
-    ASSERT_EQ(collectionInfo.records.size(), 1);
-    ASSERT_TRUE(isEqual(collectionInfo.records[0], expected))
-            << "Periodic collection record doesn't match.\nExpected:\n"
-            << toString(expected) << "\nActual:\n"
-            << toString(collectionInfo.records[0]);
+    const CollectionInfo expected{
+            .maxCacheSize = static_cast<size_t>(sysprop::periodicCollectionBufferSize().value_or(
+                    kDefaultPeriodicCollectionBufferSize)),
+            .records = {{
+                    .systemSummaryStats = systemSummaryStats,
+                    .userPackageSummaryStats = userPackageSummaryStats,
+            }},
+    };
 
-    TemporaryFile dump;
-    ASSERT_RESULT_OK(collector->onDump(dump.fd));
+    EXPECT_THAT(actual, CollectionInfoEq(expected))
+            << "Periodic collection info doesn't match.\nExpected:\n"
+            << expected.toString() << "\nActual:\n"
+            << actual.toString();
 
-    lseek(dump.fd, 0, SEEK_SET);
-    std::string dumpContents;
-    ASSERT_TRUE(ReadFdToString(dump.fd, &dumpContents));
-    ASSERT_FALSE(dumpContents.empty());
-
-    ASSERT_EQ(countOccurrences(dumpContents, kEmptyCollectionMessage), 1)
-            << "Only boot-time collection should be not collected. Dump contents: " << dumpContents;
+    ASSERT_NO_FATAL_FAILURE(checkDumpContents(/*wantedEmptyCollectionInstances=*/1))
+            << "Boot-time collection shouldn't be reported";
 }
 
-TEST(IoPerfCollectionTest, TestCustomCollection) {
-    sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
-    sp<MockProcStat> mockProcStat = new MockProcStat();
-    sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
+TEST_F(IoPerfCollectionTest, TestOnCustomCollectionWithoutPackageFilter) {
+    const auto [uidStats, userPackageSummaryStats] = sampleUidStats();
+    const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
 
-    sp<IoPerfCollection> collector = new IoPerfCollection();
-    internal::IoPerfCollectionPeer collectorPeer(collector);
-
-    ASSERT_RESULT_OK(collectorPeer.init());
-
-    // Filter by package name should ignore this limit.
-    collectorPeer.setTopNStatsPerCategory(1);
-
-    const std::unordered_map<uid_t, UidIoUsage> uidIoUsages({
-            {1009, {.uid = 1009, .ios = {0, 14000, 0, 16000, 0, 100}}},
-            {2001, {.uid = 2001, .ios = {0, 3400, 0, 6700, 0, 200}}},
-            {3456, {.uid = 3456, .ios = {0, 4200, 0, 5600, 0, 300}}},
-    });
-    const ProcStatInfo procStatInfo{
-            /*stats=*/{2900, 7900, 4900, 8900, /*ioWaitTime=*/5900, 6966, 7980, 0, 0, 2930},
-            /*runnableCnt=*/100,
-            /*ioBlockedCnt=*/57,
-    };
-    const std::vector<ProcessStats> processStats({
-            {.tgid = 100,
-             .uid = 1009,
-             .process = {100, "cts_test", "D", 1, 50900, 2, 234},
-             .threads = {{100, {100, "cts_test", "D", 1, 50900, 1, 234}},
-                         {200, {200, "cts_test_2", "D", 1, 0, 1, 290}}}},
-            {.tgid = 1000,
-             .uid = 2001,
-             .process = {1000, "system_server", "D", 1, 1234, 1, 345},
-             .threads = {{1000, {1000, "system_server", "D", 1, 1234, 1, 345}}}},
-            {.tgid = 4000,
-             .uid = 3456,
-             .process = {4000, "random_process", "D", 1, 3456, 1, 890},
-             .threads = {{4000, {4000, "random_process", "D", 1, 50900, 1, 890}}}},
-    });
-
-    EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
-    EXPECT_CALL(*mockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
-    EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(processStats));
-    const IoPerfRecord expected = {
-            .uidIoPerfData = {.topNReads = {{.userId = 0,
-                                             .packageName = "android.car.cts",
-                                             .bytes = {0, 14000},
-                                             .fsync = {0, 100}},
-                                            {.userId = 0,
-                                             .packageName = "system_server",
-                                             .bytes = {0, 3400},
-                                             .fsync = {0, 200}}},
-                              .topNWrites = {{.userId = 0,
-                                              .packageName = "android.car.cts",
-                                              .bytes = {0, 16000},
-                                              .fsync = {0, 100}},
-                                             {.userId = 0,
-                                              .packageName = "system_server",
-                                              .bytes = {0, 6700},
-                                              .fsync = {0, 200}}},
-                              .total = {{0, 21600}, {0, 28300}, {0, 600}}},
-            .systemIoPerfData = {.cpuIoWaitTime = 5900,
-                                 .totalCpuTime = 48376,
-                                 .ioBlockedProcessesCnt = 57,
-                                 .totalProcessesCnt = 157},
-            .processIoPerfData =
-                    {.topNIoBlockedUids = {{0, "android.car.cts", 2, {{"cts_test", 2}}},
-                                           {0, "system_server", 1, {{"system_server", 1}}}},
-                     .topNIoBlockedUidsTotalTaskCnt = {2, 1},
-                     .topNMajorFaultUids = {{0, "android.car.cts", 50900, {{"cts_test", 50900}}},
-                                            {0, "system_server", 1234, {{"system_server", 1234}}}},
-                     .totalMajorFaults = 55590,
-                     .majorFaultsPercentChange = 0},
-    };
-    collectorPeer.injectUidToPackageNameMapping({
-            {1009, "android.car.cts"},
-            {2001, "system_server"},
-            {3456, "random_process"},
-    });
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+    EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
 
     time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
-    ASSERT_RESULT_OK(collector->onCustomCollection(now, SystemState::NORMAL_MODE,
-                                                   {"android.car.cts", "system_server"},
-                                                   mockUidIoStats, mockProcStat, mockProcPidStat));
+    ASSERT_RESULT_OK(mCollector->onCustomCollection(now, SystemState::NORMAL_MODE, {},
+                                                    mMockUidStatsCollector, mMockProcStat));
 
-    const CollectionInfo& collectionInfo = collectorPeer.getCustomCollectionInfo();
+    const auto actual = mCollectorPeer->getCustomCollectionInfo();
 
-    EXPECT_EQ(collectionInfo.maxCacheSize, std::numeric_limits<std::size_t>::max());
-    ASSERT_EQ(collectionInfo.records.size(), 1);
-    ASSERT_TRUE(isEqual(collectionInfo.records[0], expected))
-            << "Custom collection record doesn't match.\nExpected:\n"
-            << toString(expected) << "\nActual:\n"
-            << toString(collectionInfo.records[0]);
+    CollectionInfo expected{
+            .maxCacheSize = std::numeric_limits<std::size_t>::max(),
+            .records = {{
+                    .systemSummaryStats = systemSummaryStats,
+                    .userPackageSummaryStats = userPackageSummaryStats,
+            }},
+    };
+
+    EXPECT_THAT(actual, CollectionInfoEq(expected))
+            << "Custom collection info doesn't match.\nExpected:\n"
+            << expected.toString() << "\nActual:\n"
+            << actual.toString();
+
+    ASSERT_NO_FATAL_FAILURE(checkCustomDumpContents()) << "Custom collection should be reported";
 
     TemporaryFile customDump;
-    ASSERT_RESULT_OK(collector->onCustomCollectionDump(customDump.fd));
-
-    lseek(customDump.fd, 0, SEEK_SET);
-    std::string customDumpContents;
-    ASSERT_TRUE(ReadFdToString(customDump.fd, &customDumpContents));
-    ASSERT_FALSE(customDumpContents.empty());
-    ASSERT_EQ(countOccurrences(customDumpContents, kEmptyCollectionMessage), 0)
-            << "Custom collection should be reported. Dump contents: " << customDumpContents;
+    ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(customDump.fd));
 
     // Should clear the cache.
-    ASSERT_RESULT_OK(collector->onCustomCollectionDump(-1));
+    ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(-1));
 
-    const CollectionInfo& emptyCollectionInfo = collectorPeer.getCustomCollectionInfo();
-    EXPECT_TRUE(emptyCollectionInfo.records.empty());
-    EXPECT_EQ(emptyCollectionInfo.maxCacheSize, std::numeric_limits<std::size_t>::max());
+    expected.records.clear();
+    const CollectionInfo& emptyCollectionInfo = mCollectorPeer->getCustomCollectionInfo();
+    EXPECT_THAT(emptyCollectionInfo, CollectionInfoEq(expected))
+            << "Custom collection should be cleared.";
 }
 
-TEST(IoPerfCollectionTest, TestUidIoStatsGreaterThanTopNStatsLimit) {
-    std::unordered_map<uid_t, UidIoUsage> uidIoUsages({
-            {1001234, {.uid = 1001234, .ios = {3000, 0, 500, 0, 20, 0}}},
-            {1005678, {.uid = 1005678, .ios = {30, 100, 50, 200, 45, 60}}},
-            {1009, {.uid = 1009, .ios = {0, 20000, 0, 30000, 0, 300}}},
-            {1001000, {.uid = 1001000, .ios = {2000, 200, 1000, 100, 50, 10}}},
-    });
-    sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
-    EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
+TEST_F(IoPerfCollectionTest, TestOnCustomCollectionWithPackageFilter) {
+    // Filter by package name should ignore this limit with package filter.
+    mCollectorPeer->setTopNStatsPerCategory(1);
 
-    struct UidIoPerfData expectedUidIoPerfData = {
-            .topNReads = {{.userId = 0,  // uid: 1009
-                           .packageName = "mount",
-                           .bytes = {0, 20000},
-                           .fsync = {0, 300}},
-                          {.userId = 10,  // uid: 1001234
-                           .packageName = "1001234",
-                           .bytes = {3000, 0},
-                           .fsync = {20, 0}}},
-            .topNWrites = {{.userId = 0,  // uid: 1009
-                            .packageName = "mount",
-                            .bytes = {0, 30000},
-                            .fsync = {0, 300}},
-                           {.userId = 10,  // uid: 1001000
-                            .packageName = "shared:android.uid.system",
-                            .bytes = {1000, 100},
-                            .fsync = {50, 10}}},
-            .total = {{5030, 20300}, {1550, 30300}, {115, 370}},
-    };
+    const auto [uidStats, _] = sampleUidStats();
+    const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
 
-    IoPerfCollection collector;
-    collector.mTopNStatsPerCategory = 2;
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+    EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
 
-    sp<MockPackageInfoResolver> mockPackageInfoResolver = new MockPackageInfoResolver();
-    collector.mPackageInfoResolver = mockPackageInfoResolver;
-    EXPECT_CALL(*mockPackageInfoResolver, getPackageNamesForUids(_))
-            .WillRepeatedly(Return<std::unordered_map<uid_t, std::string>>(
-                    {{1009, "mount"}, {1001000, "shared:android.uid.system"}}));
+    time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+    ASSERT_RESULT_OK(mCollector->onCustomCollection(now, SystemState::NORMAL_MODE,
+                                                    {"mount", "com.google.android.car.kitchensink"},
+                                                    mMockUidStatsCollector, mMockProcStat));
 
-    struct UidIoPerfData actualUidIoPerfData = {};
-    collector.processUidIoPerfData({}, mockUidIoStats, &actualUidIoPerfData);
+    const auto actual = mCollectorPeer->getCustomCollectionInfo();
 
-    EXPECT_TRUE(isEqual(expectedUidIoPerfData, actualUidIoPerfData))
-        << "First snapshot doesn't match.\nExpected:\n"
-        << toString(expectedUidIoPerfData) << "\nActual:\n"
-        << toString(actualUidIoPerfData);
-
-    uidIoUsages = {
-            {1001234, {.uid = 1001234, .ios = {4000, 0, 450, 0, 25, 0}}},
-            {1005678, {.uid = 1005678, .ios = {10, 900, 0, 400, 5, 10}}},
-            {1003456, {.uid = 1003456, .ios = {200, 0, 300, 0, 50, 0}}},
-            {1001000, {.uid = 1001000, .ios = {0, 0, 0, 0, 0, 0}}},
-    };
-    EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
-
-    expectedUidIoPerfData = {
-            .topNReads = {{.userId = 10,  // uid: 1001234
-                           .packageName = "1001234",
-                           .bytes = {4000, 0},
-                           .fsync = {25, 0}},
-                          {.userId = 10,  // uid: 1005678
-                           .packageName = "1005678",
-                           .bytes = {10, 900},
-                           .fsync = {5, 10}}},
-            .topNWrites = {{.userId = 10,  // uid: 1001234
-                            .packageName = "1001234",
-                            .bytes = {450, 0},
-                            .fsync = {25, 0}},
-                           {.userId = 10,  // uid: 1005678
-                            .packageName = "1005678",
-                            .bytes = {0, 400},
-                            .fsync = {5, 10}}},
-            .total = {{4210, 900}, {750, 400}, {80, 10}},
-    };
-    actualUidIoPerfData = {};
-    collector.processUidIoPerfData({}, mockUidIoStats, &actualUidIoPerfData);
-
-    EXPECT_TRUE(isEqual(expectedUidIoPerfData, actualUidIoPerfData))
-        << "Second snapshot doesn't match.\nExpected:\n"
-        << toString(expectedUidIoPerfData) << "\nActual:\n"
-        << toString(actualUidIoPerfData);
-}
-
-TEST(IoPerfCollectionTest, TestUidIOStatsLessThanTopNStatsLimit) {
-    const std::unordered_map<uid_t, UidIoUsage> uidIoUsages(
-            {{1001234, {.uid = 1001234, .ios = {3000, 0, 500, 0, 20, 0}}}});
-
-    const struct UidIoPerfData expectedUidIoPerfData = {
-            .topNReads = {{.userId = 10,
-                           .packageName = "1001234",
-                           .bytes = {3000, 0},
-                           .fsync = {20, 0}}},
-            .topNWrites =
-                    {{.userId = 10, .packageName = "1001234", .bytes = {500, 0}, .fsync = {20, 0}}},
-            .total = {{3000, 0}, {500, 0}, {20, 0}},
-    };
-
-    sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
-    EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
-
-    IoPerfCollection collector;
-    collector.mTopNStatsPerCategory = 10;
-
-    struct UidIoPerfData actualUidIoPerfData = {};
-    collector.processUidIoPerfData({}, mockUidIoStats, &actualUidIoPerfData);
-
-    EXPECT_TRUE(isEqual(expectedUidIoPerfData, actualUidIoPerfData))
-        << "Collected data doesn't match.\nExpected:\n"
-        << toString(expectedUidIoPerfData) << "\nActual:\n"
-        << toString(actualUidIoPerfData);
-}
-
-TEST(IoPerfCollectionTest, TestProcessSystemIoPerfData) {
-    const ProcStatInfo procStatInfo(
-            /*stats=*/{6200, 5700, 1700, 3100, 1100, 5200, 3900, 0, 0, 0},
-            /*runnableCnt=*/17,
-            /*ioBlockedCnt=*/5);
-    struct SystemIoPerfData expectedSystemIoPerfData = {
-            .cpuIoWaitTime = 1100,
-            .totalCpuTime = 26900,
-            .ioBlockedProcessesCnt = 5,
-            .totalProcessesCnt = 22,
-    };
-
-    sp<MockProcStat> mockProcStat = new MockProcStat();
-    EXPECT_CALL(*mockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
-
-    IoPerfCollection collector;
-    struct SystemIoPerfData actualSystemIoPerfData = {};
-    collector.processSystemIoPerfData(mockProcStat, &actualSystemIoPerfData);
-
-    EXPECT_TRUE(isEqual(expectedSystemIoPerfData, actualSystemIoPerfData))
-            << "Expected:\n"
-            << toString(expectedSystemIoPerfData) << "\nActual:\n"
-            << toString(actualSystemIoPerfData);
-}
-
-TEST(IoPerfCollectionTest, TestProcPidContentsGreaterThanTopNStatsLimit) {
-    const std::vector<ProcessStats> firstProcessStats({
-            {.tgid = 1,
-             .uid = 0,
-             .process = {1, "init", "S", 0, 220, 2, 0},
-             .threads = {{1, {1, "init", "S", 0, 200, 2, 0}},
-                         {453, {453, "init", "S", 0, 20, 2, 275}}}},
-            {.tgid = 2456,
-             .uid = 1001000,
-             .process = {2456, "system_server", "R", 1, 6000, 3, 1000},
-             .threads = {{2456, {2456, "system_server", "R", 1, 1000, 3, 1000}},
-                         {3456, {3456, "system_server", "S", 1, 3000, 3, 2300}},
-                         {4789, {4789, "system_server", "D", 1, 2000, 3, 4500}}}},
-            {.tgid = 7890,
-             .uid = 1001000,
-             .process = {7890, "logd", "D", 1, 15000, 3, 2345},
-             .threads = {{7890, {7890, "logd", "D", 1, 10000, 3, 2345}},
-                         {8978, {8978, "logd", "D", 1, 1000, 3, 2500}},
-                         {12890, {12890, "logd", "D", 1, 500, 3, 2900}}}},
-            {.tgid = 18902,
-             .uid = 1009,
-             .process = {18902, "disk I/O", "D", 1, 45678, 3, 897654},
-             .threads = {{18902, {18902, "disk I/O", "D", 1, 30000, 3, 897654}},
-                         {21345, {21345, "disk I/O", "D", 1, 15000, 3, 904000}},
-                         {32452, {32452, "disk I/O", "D", 1, 678, 3, 1007000}}}},
-            {.tgid = 28900,
-             .uid = 1001234,
-             .process = {28900, "tombstoned", "D", 1, 89765, 1, 2345671},
-             .threads = {{28900, {28900, "tombstoned", "D", 1, 89765, 1, 2345671}}}},
-    });
-    sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
-    EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(firstProcessStats));
-
-    struct ProcessIoPerfData expectedProcessIoPerfData = {
-            .topNIoBlockedUids = {{.userId = 10,  // uid: 1001000
-                                   .packageName = "shared:android.uid.system",
-                                   .count = 4,
-                                   .topNProcesses = {{"logd", 3}, {"system_server", 1}}},
-                                  {.userId = 0,
-                                   .packageName = "mount",
-                                   .count = 3,
-                                   .topNProcesses = {{"disk I/O", 3}}}},
-            .topNIoBlockedUidsTotalTaskCnt = {6, 3},
-            .topNMajorFaultUids = {{.userId = 10,  // uid: 1001234
-                                    .packageName = "1001234",
-                                    .count = 89765,
-                                    .topNProcesses = {{"tombstoned", 89765}}},
-                                   {.userId = 0,  // uid: 1009
-                                    .packageName = "mount",
-                                    .count = 45678,
-                                    .topNProcesses = {{"disk I/O", 45678}}}},
-            .totalMajorFaults = 156663,
+    UserPackageSummaryStats userPackageSummaryStats{
+            .topNIoReads = {{1009, "mount", UserPackageStats::IoStats{{0, 14'000}, {0, 100}}},
+                            {1002001, "com.google.android.car.kitchensink",
+                             UserPackageStats::IoStats{{0, 3'400}, {0, 200}}}},
+            .topNIoWrites = {{1009, "mount", UserPackageStats::IoStats{{0, 16'000}, {0, 100}}},
+                             {1002001, "com.google.android.car.kitchensink",
+                              UserPackageStats::IoStats{{0, 6'700}, {0, 200}}}},
+            .topNIoBlocked = {{1009, "mount", UserPackageStats::ProcStats{1, {{"disk I/O", 1}}}},
+                              {1002001, "com.google.android.car.kitchensink",
+                               UserPackageStats::ProcStats{3,
+                                                           {{"CTS", 2}, {"KitchenSinkApp", 1}}}}},
+            .topNMajorFaults =
+                    {{1009, "mount", UserPackageStats::ProcStats{11'000, {{"disk I/O", 11'000}}}},
+                     {1002001, "com.google.android.car.kitchensink",
+                      UserPackageStats::ProcStats{22'445,
+                                                  {{"KitchenSinkApp", 12'345}, {"CTS", 10'100}}}}},
+            .totalIoStats = {{1000, 21'600}, {300, 28'300}, {600, 600}},
+            .taskCountByUid = {{1009, 1}, {1002001, 5}},
+            .totalMajorFaults = 84'345,
             .majorFaultsPercentChange = 0.0,
     };
 
-    IoPerfCollection collector;
-    collector.mTopNStatsPerCategory = 2;
-    collector.mTopNStatsPerSubcategory = 2;
-
-    sp<MockPackageInfoResolver> mockPackageInfoResolver = new MockPackageInfoResolver();
-    collector.mPackageInfoResolver = mockPackageInfoResolver;
-    EXPECT_CALL(*mockPackageInfoResolver, getPackageNamesForUids(_))
-            .WillRepeatedly(Return<std::unordered_map<uid_t, std::string>>(
-                    {{0, "root"}, {1009, "mount"}, {1001000, "shared:android.uid.system"}}));
-
-    struct ProcessIoPerfData actualProcessIoPerfData = {};
-    collector.processProcessIoPerfDataLocked({}, mockProcPidStat, &actualProcessIoPerfData);
-
-    EXPECT_TRUE(isEqual(expectedProcessIoPerfData, actualProcessIoPerfData))
-            << "First snapshot doesn't match.\nExpected:\n"
-            << toString(expectedProcessIoPerfData) << "\nActual:\n"
-            << toString(actualProcessIoPerfData);
-
-    const std::vector<ProcessStats> secondProcessStats({
-            {.tgid = 1,
-             .uid = 0,
-             .process = {1, "init", "S", 0, 660, 2, 0},
-             .threads = {{1, {1, "init", "S", 0, 600, 2, 0}},
-                         {453, {453, "init", "S", 0, 60, 2, 275}}}},
-            {.tgid = 2546,
-             .uid = 1001000,
-             .process = {2546, "system_server", "R", 1, 12000, 3, 1000},
-             .threads = {{2456, {2456, "system_server", "R", 1, 2000, 3, 1000}},
-                         {3456, {3456, "system_server", "S", 1, 6000, 3, 2300}},
-                         {4789, {4789, "system_server", "D", 1, 4000, 3, 4500}}}},
-    });
-    EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(secondProcessStats));
-    expectedProcessIoPerfData = {
-            .topNIoBlockedUids = {{.userId = 10,  // uid: 1001000
-                                   .packageName = "shared:android.uid.system",
-                                   .count = 1,
-                                   .topNProcesses = {{"system_server", 1}}}},
-            .topNIoBlockedUidsTotalTaskCnt = {3},
-            .topNMajorFaultUids = {{.userId = 10,  // uid: 1001000
-                                    .packageName = "shared:android.uid.system",
-                                    .count = 12000,
-                                    .topNProcesses = {{"system_server", 12000}}},
-                                   {.userId = 0,  // uid: 0
-                                    .packageName = "root",
-                                    .count = 660,
-                                    .topNProcesses = {{"init", 660}}}},
-            .totalMajorFaults = 12660,
-            .majorFaultsPercentChange = ((12660.0 - 156663.0) / 156663.0) * 100,
+    CollectionInfo expected{
+            .maxCacheSize = std::numeric_limits<std::size_t>::max(),
+            .records = {{
+                    .systemSummaryStats = systemSummaryStats,
+                    .userPackageSummaryStats = userPackageSummaryStats,
+            }},
     };
 
-    actualProcessIoPerfData = {};
-    collector.processProcessIoPerfDataLocked({}, mockProcPidStat, &actualProcessIoPerfData);
+    EXPECT_THAT(actual, CollectionInfoEq(expected))
+            << "Custom collection info doesn't match.\nExpected:\n"
+            << expected.toString() << "\nActual:\n"
+            << actual.toString();
 
-    EXPECT_TRUE(isEqual(expectedProcessIoPerfData, actualProcessIoPerfData))
-            << "Second snapshot doesn't match.\nExpected:\n"
-            << toString(expectedProcessIoPerfData) << "\nActual:\n"
-            << toString(actualProcessIoPerfData);
+    ASSERT_NO_FATAL_FAILURE(checkCustomDumpContents()) << "Custom collection should be reported";
+
+    TemporaryFile customDump;
+    ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(customDump.fd));
+
+    // Should clear the cache.
+    ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(-1));
+
+    expected.records.clear();
+    const CollectionInfo& emptyCollectionInfo = mCollectorPeer->getCustomCollectionInfo();
+    EXPECT_THAT(emptyCollectionInfo, CollectionInfoEq(expected))
+            << "Custom collection should be cleared.";
 }
 
-TEST(IoPerfCollectionTest, TestProcPidContentsLessThanTopNStatsLimit) {
-    const std::vector<ProcessStats> processStats({
-            {.tgid = 1,
-             .uid = 0,
-             .process = {1, "init", "S", 0, 880, 2, 0},
-             .threads = {{1, {1, "init", "S", 0, 800, 2, 0}},
-                         {453, {453, "init", "S", 0, 80, 2, 275}}}},
-    });
-    sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
-    EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(processStats));
+TEST_F(IoPerfCollectionTest, TestOnPeriodicCollectionWithTrimmingStatsAfterTopN) {
+    mCollectorPeer->setTopNStatsPerCategory(1);
+    mCollectorPeer->setTopNStatsPerSubcategory(1);
 
-    struct ProcessIoPerfData expectedProcessIoPerfData = {
-            .topNMajorFaultUids = {{.userId = 0,  // uid: 0
-                                    .packageName = "root",
-                                    .count = 880,
-                                    .topNProcesses = {{"init", 880}}}},
-            .totalMajorFaults = 880,
+    const auto [uidStats, _] = sampleUidStats();
+    const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
+
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+    EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
+
+    time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+    ASSERT_RESULT_OK(mCollector->onPeriodicCollection(now, SystemState::NORMAL_MODE,
+                                                      mMockUidStatsCollector, mMockProcStat));
+
+    const auto actual = mCollectorPeer->getPeriodicCollectionInfo();
+
+    UserPackageSummaryStats userPackageSummaryStats{
+            .topNIoReads = {{1009, "mount", UserPackageStats::IoStats{{0, 14'000}, {0, 100}}}},
+            .topNIoWrites = {{1009, "mount", UserPackageStats::IoStats{{0, 16'000}, {0, 100}}}},
+            .topNIoBlocked = {{1002001, "com.google.android.car.kitchensink",
+                               UserPackageStats::ProcStats{3, {{"CTS", 2}}}}},
+            .topNMajorFaults = {{1012345, "1012345",
+                                 UserPackageStats::ProcStats{50'900, {{"MapsApp", 50'900}}}}},
+            .totalIoStats = {{1000, 21'600}, {300, 28'300}, {600, 600}},
+            .taskCountByUid = {{1009, 1}, {1002001, 5}, {1012345, 4}},
+            .totalMajorFaults = 84'345,
             .majorFaultsPercentChange = 0.0,
     };
 
-    IoPerfCollection collector;
-    collector.mTopNStatsPerCategory = 5;
-    collector.mTopNStatsPerSubcategory = 3;
+    const CollectionInfo expected{
+            .maxCacheSize = static_cast<size_t>(sysprop::periodicCollectionBufferSize().value_or(
+                    kDefaultPeriodicCollectionBufferSize)),
+            .records = {{
+                    .systemSummaryStats = systemSummaryStats,
+                    .userPackageSummaryStats = userPackageSummaryStats,
+            }},
+    };
 
-    sp<MockPackageInfoResolver> mockPackageInfoResolver = new MockPackageInfoResolver();
-    collector.mPackageInfoResolver = mockPackageInfoResolver;
-    EXPECT_CALL(*mockPackageInfoResolver, getPackageNamesForUids(_))
-            .WillRepeatedly(Return<std::unordered_map<uid_t, std::string>>({{0, "root"}}));
+    EXPECT_THAT(actual, CollectionInfoEq(expected))
+            << "Periodic collection info doesn't match.\nExpected:\n"
+            << expected.toString() << "\nActual:\n"
+            << actual.toString();
 
-    struct ProcessIoPerfData actualProcessIoPerfData = {};
-    collector.processProcessIoPerfDataLocked({}, mockProcPidStat, &actualProcessIoPerfData);
+    ASSERT_NO_FATAL_FAILURE(checkDumpContents(/*wantedEmptyCollectionInstances=*/1))
+            << "Boot-time collection shouldn't be reported";
+}
 
-    EXPECT_TRUE(isEqual(expectedProcessIoPerfData, actualProcessIoPerfData))
-            << "proc pid contents don't match.\nExpected:\n"
-            << toString(expectedProcessIoPerfData) << "\nActual:\n"
-            << toString(actualProcessIoPerfData);
+TEST_F(IoPerfCollectionTest, TestConsecutiveOnPeriodicCollection) {
+    const auto [firstUidStats, firstUserPackageSummaryStats] = sampleUidStats();
+    const auto [firstProcStatInfo, firstSystemSummaryStats] = sampleProcStat();
+
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(firstUidStats));
+    EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(firstProcStatInfo));
+
+    time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+    ASSERT_RESULT_OK(mCollector->onPeriodicCollection(now, SystemState::NORMAL_MODE,
+                                                      mMockUidStatsCollector, mMockProcStat));
+
+    auto [secondUidStats, secondUserPackageSummaryStats] = sampleUidStats(/*multiplier=*/2);
+    const auto [secondProcStatInfo, secondSystemSummaryStats] = sampleProcStat(/*multiplier=*/2);
+
+    secondUserPackageSummaryStats.majorFaultsPercentChange =
+            (static_cast<double>(secondUserPackageSummaryStats.totalMajorFaults -
+                                 firstUserPackageSummaryStats.totalMajorFaults) /
+             static_cast<double>(firstUserPackageSummaryStats.totalMajorFaults)) *
+            100.0;
+
+    EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(secondUidStats));
+    EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(secondProcStatInfo));
+
+    ASSERT_RESULT_OK(mCollector->onPeriodicCollection(now, SystemState::NORMAL_MODE,
+                                                      mMockUidStatsCollector, mMockProcStat));
+
+    const auto actual = mCollectorPeer->getPeriodicCollectionInfo();
+
+    const CollectionInfo expected{
+            .maxCacheSize = static_cast<size_t>(sysprop::periodicCollectionBufferSize().value_or(
+                    kDefaultPeriodicCollectionBufferSize)),
+            .records = {{.systemSummaryStats = firstSystemSummaryStats,
+                         .userPackageSummaryStats = firstUserPackageSummaryStats},
+                        {.systemSummaryStats = secondSystemSummaryStats,
+                         .userPackageSummaryStats = secondUserPackageSummaryStats}},
+    };
+
+    EXPECT_THAT(actual, CollectionInfoEq(expected))
+            << "Periodic collection info doesn't match.\nExpected:\n"
+            << expected.toString() << "\nActual:\n"
+            << actual.toString();
+
+    ASSERT_NO_FATAL_FAILURE(checkDumpContents(/*wantedEmptyCollectionInstances=*/1))
+            << "Boot-time collection shouldn't be reported";
 }
 
 }  // namespace watchdog
diff --git a/cpp/watchdog/server/tests/MockDataProcessor.h b/cpp/watchdog/server/tests/MockDataProcessor.h
index d9a2600..b9ea48c 100644
--- a/cpp/watchdog/server/tests/MockDataProcessor.h
+++ b/cpp/watchdog/server/tests/MockDataProcessor.h
@@ -30,24 +30,22 @@
     MockDataProcessor() {
         EXPECT_CALL(*this, name()).WillRepeatedly(::testing::Return("MockedDataProcessor"));
     }
-    MOCK_METHOD(std::string, name, (), (override));
+    MOCK_METHOD(std::string, name, (), (const, override));
     MOCK_METHOD(android::base::Result<void>, init, (), (override));
     MOCK_METHOD(void, terminate, (), (override));
     MOCK_METHOD(android::base::Result<void>, onBoottimeCollection,
-                (time_t, const wp<UidIoStats>&, const wp<ProcStat>&, const wp<ProcPidStat>&),
-                (override));
+                (time_t, const wp<UidStatsCollectorInterface>&, const wp<ProcStat>&), (override));
     MOCK_METHOD(android::base::Result<void>, onPeriodicCollection,
-                (time_t, SystemState, const wp<UidIoStats>&, const wp<ProcStat>&,
-                 const wp<ProcPidStat>&),
+                (time_t, SystemState, const wp<UidStatsCollectorInterface>&, const wp<ProcStat>&),
                 (override));
     MOCK_METHOD(android::base::Result<void>, onCustomCollection,
-                (time_t, SystemState, const std::unordered_set<std::string>&, const wp<UidIoStats>&,
-                 const wp<ProcStat>&, const wp<ProcPidStat>&),
+                (time_t, SystemState, const std::unordered_set<std::string>&,
+                 const wp<UidStatsCollectorInterface>&, const wp<ProcStat>&),
                 (override));
     MOCK_METHOD(android::base::Result<void>, onPeriodicMonitor,
                 (time_t, const android::wp<IProcDiskStatsInterface>&, const std::function<void()>&),
                 (override));
-    MOCK_METHOD(android::base::Result<void>, onDump, (int), (override));
+    MOCK_METHOD(android::base::Result<void>, onDump, (int), (const, override));
     MOCK_METHOD(android::base::Result<void>, onCustomCollectionDump, (int), (override));
 };
 
diff --git a/cpp/watchdog/server/tests/MockIoOveruseConfigs.h b/cpp/watchdog/server/tests/MockIoOveruseConfigs.h
index 22c23f7..6859e18 100644
--- a/cpp/watchdog/server/tests/MockIoOveruseConfigs.h
+++ b/cpp/watchdog/server/tests/MockIoOveruseConfigs.h
@@ -43,7 +43,7 @@
     MOCK_METHOD(
             void, get,
             (std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*),
-            (override));
+            (const, override));
 
     MOCK_METHOD(android::base::Result<void>, writeToDisk, (), (override));
 
diff --git a/cpp/watchdog/server/tests/MockIoOveruseMonitor.h b/cpp/watchdog/server/tests/MockIoOveruseMonitor.h
index 79ba932..a9d677e 100644
--- a/cpp/watchdog/server/tests/MockIoOveruseMonitor.h
+++ b/cpp/watchdog/server/tests/MockIoOveruseMonitor.h
@@ -35,8 +35,8 @@
         ON_CALL(*this, name()).WillByDefault(::testing::Return("MockIoOveruseMonitor"));
     }
     ~MockIoOveruseMonitor() {}
-    MOCK_METHOD(bool, isInitialized, (), (override));
-    MOCK_METHOD(bool, dumpHelpText, (int), (override));
+    MOCK_METHOD(bool, isInitialized, (), (const, override));
+    MOCK_METHOD(bool, dumpHelpText, (int), (const, override));
     MOCK_METHOD(android::base::Result<void>, updateResourceOveruseConfigurations,
                 (const std::vector<
                         android::automotive::watchdog::internal::ResourceOveruseConfiguration>&),
@@ -44,7 +44,7 @@
     MOCK_METHOD(
             android::base::Result<void>, getResourceOveruseConfigurations,
             (std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*),
-            (override));
+            (const, override));
     MOCK_METHOD(android::base::Result<void>, actionTakenOnIoOveruse,
                 (const std::vector<
                         android::automotive::watchdog::internal::PackageResourceOveruseAction>&
@@ -54,7 +54,8 @@
                 (const sp<IResourceOveruseListener>&), (override));
     MOCK_METHOD(android::base::Result<void>, removeIoOveruseListener,
                 (const sp<IResourceOveruseListener>&), (override));
-    MOCK_METHOD(android::base::Result<void>, getIoOveruseStats, (IoOveruseStats*), (override));
+    MOCK_METHOD(android::base::Result<void>, getIoOveruseStats, (IoOveruseStats*),
+                (const, override));
     MOCK_METHOD(android::base::Result<void>, resetIoOveruseStats, (const std::vector<std::string>&),
                 (override));
 };
diff --git a/cpp/watchdog/server/tests/MockProcPidStat.h b/cpp/watchdog/server/tests/MockProcPidStat.h
deleted file mode 100644
index acc8b0c..0000000
--- a/cpp/watchdog/server/tests/MockProcPidStat.h
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKPROCPIDSTAT_H_
-#define CPP_WATCHDOG_SERVER_TESTS_MOCKPROCPIDSTAT_H_
-
-#include "ProcPidStat.h"
-
-#include <android-base/result.h>
-#include <gmock/gmock.h>
-
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-class MockProcPidStat : public ProcPidStat {
-public:
-    MockProcPidStat() { ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true)); }
-    MOCK_METHOD(bool, enabled, (), (override));
-    MOCK_METHOD(android::base::Result<void>, collect, (), (override));
-    MOCK_METHOD((const std::unordered_map<pid_t, ProcessStats>), latestStats, (),
-                (const, override));
-    MOCK_METHOD(const std::vector<ProcessStats>, deltaStats, (), (const, override));
-    MOCK_METHOD(std::string, dirPath, (), (override));
-};
-
-}  // namespace watchdog
-}  // namespace automotive
-}  // namespace android
-
-#endif  //  CPP_WATCHDOG_SERVER_TESTS_MOCKPROCPIDSTAT_H_
diff --git a/cpp/watchdog/server/tests/MockUidIoStats.h b/cpp/watchdog/server/tests/MockUidIoStats.h
deleted file mode 100644
index cf30d9f..0000000
--- a/cpp/watchdog/server/tests/MockUidIoStats.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATS_H_
-#define CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATS_H_
-
-#include "UidIoStats.h"
-
-#include <android-base/result.h>
-#include <gmock/gmock.h>
-
-#include <string>
-#include <unordered_map>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-class MockUidIoStats : public UidIoStats {
-public:
-    MockUidIoStats() { ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true)); }
-    MOCK_METHOD(bool, enabled, (), (override));
-    MOCK_METHOD(android::base::Result<void>, collect, (), (override));
-    MOCK_METHOD((const std::unordered_map<uid_t, UidIoUsage>), latestStats, (), (const, override));
-    MOCK_METHOD((const std::unordered_map<uid_t, UidIoUsage>), deltaStats, (), (const, override));
-    MOCK_METHOD(std::string, filePath, (), (override));
-
-    void expectDeltaStats(const std::unordered_map<uid_t, IoUsage>& deltaStats) {
-        std::unordered_map<uid_t, UidIoUsage> stats;
-        for (const auto& [uid, ios] : deltaStats) {
-            stats[uid] = UidIoUsage{.uid = uid, .ios = ios};
-        }
-        EXPECT_CALL(*this, deltaStats()).WillOnce(::testing::Return(stats));
-    }
-};
-
-}  // namespace watchdog
-}  // namespace automotive
-}  // namespace android
-
-#endif  //  CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATS_H_
diff --git a/cpp/watchdog/server/tests/MockUidIoStatsCollector.h b/cpp/watchdog/server/tests/MockUidIoStatsCollector.h
new file mode 100644
index 0000000..7fe8fc9
--- /dev/null
+++ b/cpp/watchdog/server/tests/MockUidIoStatsCollector.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATSCOLLECTOR_H_
+
+#include "UidIoStatsCollector.h"
+
+#include <android-base/result.h>
+#include <gmock/gmock.h>
+
+#include <string>
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+class MockUidIoStatsCollector : public UidIoStatsCollectorInterface {
+public:
+    MockUidIoStatsCollector() { ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true)); }
+    MOCK_METHOD(android::base::Result<void>, collect, (), (override));
+    MOCK_METHOD((const std::unordered_map<uid_t, UidIoStats>), latestStats, (), (const, override));
+    MOCK_METHOD((const std::unordered_map<uid_t, UidIoStats>), deltaStats, (), (const, override));
+    MOCK_METHOD(bool, enabled, (), (const, override));
+    MOCK_METHOD(const std::string, filePath, (), (const, override));
+};
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
+
+#endif  //  CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/tests/MockUidProcStatsCollector.h b/cpp/watchdog/server/tests/MockUidProcStatsCollector.h
new file mode 100644
index 0000000..a16fa37
--- /dev/null
+++ b/cpp/watchdog/server/tests/MockUidProcStatsCollector.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKUIDPROCSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_TESTS_MOCKUIDPROCSTATSCOLLECTOR_H_
+
+#include "UidProcStatsCollector.h"
+
+#include <android-base/result.h>
+#include <gmock/gmock.h>
+
+#include <string>
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+class MockUidProcStatsCollector : public UidProcStatsCollectorInterface {
+public:
+    MockUidProcStatsCollector() {
+        ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true));
+    }
+    MOCK_METHOD(android::base::Result<void>, collect, (), (override));
+    MOCK_METHOD((const std::unordered_map<uid_t, UidProcStats>), latestStats, (),
+                (const, override));
+    MOCK_METHOD((const std::unordered_map<uid_t, UidProcStats>), deltaStats, (), (const, override));
+    MOCK_METHOD(bool, enabled, (), (const, override));
+    MOCK_METHOD(const std::string, dirPath, (), (const, override));
+};
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
+
+#endif  //  CPP_WATCHDOG_SERVER_TESTS_MOCKUIDPROCSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/tests/MockUidStatsCollector.h b/cpp/watchdog/server/tests/MockUidStatsCollector.h
new file mode 100644
index 0000000..45671c1
--- /dev/null
+++ b/cpp/watchdog/server/tests/MockUidStatsCollector.h
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKUIDSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_TESTS_MOCKUIDSTATSCOLLECTOR_H_
+
+#include "UidIoStatsCollector.h"
+
+#include <android-base/result.h>
+#include <gmock/gmock.h>
+
+#include <string>
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+class MockUidStatsCollector : public UidStatsCollectorInterface {
+public:
+    MockUidStatsCollector() { ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true)); }
+    MOCK_METHOD(android::base::Result<void>, collect, (), (override));
+    MOCK_METHOD((const std::vector<UidStats>), latestStats, (), (const, override));
+    MOCK_METHOD((const std::vector<UidStats>), deltaStats, (), (const, override));
+    MOCK_METHOD(bool, enabled, (), (const, override));
+};
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
+
+#endif  //  CPP_WATCHDOG_SERVER_TESTS_MOCKUIDSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/tests/MockWatchdogPerfService.h b/cpp/watchdog/server/tests/MockWatchdogPerfService.h
index 6f0fb51..114c308 100644
--- a/cpp/watchdog/server/tests/MockWatchdogPerfService.h
+++ b/cpp/watchdog/server/tests/MockWatchdogPerfService.h
@@ -40,8 +40,8 @@
     MOCK_METHOD(android::base::Result<void>, onBootFinished, (), (override));
     MOCK_METHOD(android::base::Result<void>, onCustomCollection,
                 (int fd, const Vector<android::String16>& args), (override));
-    MOCK_METHOD(android::base::Result<void>, onDump, (int fd), (override));
-    MOCK_METHOD(bool, dumpHelpText, (int fd), (override));
+    MOCK_METHOD(android::base::Result<void>, onDump, (int fd), (const, override));
+    MOCK_METHOD(bool, dumpHelpText, (int fd), (const, override));
     MOCK_METHOD(void, handleMessage, (const Message&), (override));
 };
 
diff --git a/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.cpp b/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.cpp
index 1bf270d..9b67caf 100644
--- a/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.cpp
+++ b/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.cpp
@@ -154,15 +154,15 @@
     return threshold;
 }
 
-Matcher<const ResourceOveruseConfiguration> ResourceOveruseConfigurationMatcher(
+Matcher<const ResourceOveruseConfiguration&> ResourceOveruseConfigurationMatcher(
         const ResourceOveruseConfiguration& config) {
-    std::vector<Matcher<const ResourceSpecificConfiguration>> resourceSpecificConfigMatchers;
+    std::vector<Matcher<const ResourceSpecificConfiguration&>> resourceSpecificConfigMatchers;
     for (const auto& resourceSpecificConfig : config.resourceSpecificConfigurations) {
         resourceSpecificConfigMatchers.push_back(
                 IsResourceSpecificConfiguration(resourceSpecificConfig));
     }
 
-    std::vector<Matcher<const PackageMetadata>> metadataMatchers;
+    std::vector<Matcher<const PackageMetadata&>> metadataMatchers;
     for (const auto& metadata : config.packageMetadata) {
         metadataMatchers.push_back(IsPackageMetadata(metadata));
     }
diff --git a/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.h b/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.h
index 84c970a..a8991c7 100644
--- a/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.h
+++ b/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.h
@@ -77,7 +77,7 @@
 android::automotive::watchdog::internal::IoOveruseAlertThreshold toIoOveruseAlertThreshold(
         const int64_t durationInSeconds, const int64_t writtenBytesPerSecond);
 
-testing::Matcher<const android::automotive::watchdog::internal::ResourceOveruseConfiguration>
+testing::Matcher<const android::automotive::watchdog::internal::ResourceOveruseConfiguration&>
 ResourceOveruseConfigurationMatcher(
         const android::automotive::watchdog::internal::ResourceOveruseConfiguration& config);
 
diff --git a/cpp/watchdog/server/tests/PackageInfoResolverTest.cpp b/cpp/watchdog/server/tests/PackageInfoResolverTest.cpp
index f2fe3a6..cf68dda 100644
--- a/cpp/watchdog/server/tests/PackageInfoResolverTest.cpp
+++ b/cpp/watchdog/server/tests/PackageInfoResolverTest.cpp
@@ -16,6 +16,7 @@
 
 #include "MockWatchdogServiceHelper.h"
 #include "PackageInfoResolver.h"
+#include "PackageInfoTestUtils.h"
 
 #include <android-base/stringprintf.h>
 #include <android/automotive/watchdog/internal/ApplicationCategoryType.h>
@@ -48,20 +49,6 @@
         std::unordered_map<std::string,
                            android::automotive::watchdog::internal::ApplicationCategoryType>;
 
-PackageInfo constructPackageInfo(const char* packageName, int32_t uid, UidType uidType,
-                                 ComponentType componentType,
-                                 ApplicationCategoryType appCategoryType,
-                                 std::vector<std::string> sharedUidPackages = {}) {
-    PackageInfo packageInfo;
-    packageInfo.packageIdentifier.name = packageName;
-    packageInfo.packageIdentifier.uid = uid;
-    packageInfo.uidType = uidType;
-    packageInfo.componentType = componentType;
-    packageInfo.appCategoryType = appCategoryType;
-    packageInfo.sharedUidPackages = sharedUidPackages;
-    return packageInfo;
-}
-
 std::string toString(const std::unordered_map<uid_t, PackageInfo>& mappings) {
     std::string buffer = "{";
     for (const auto& [uid, info] : mappings) {
@@ -195,6 +182,8 @@
             // system.package.B is native package so this should be ignored.
             {"system.package.B", ApplicationCategoryType::MAPS},
             {"vendor.package.A", ApplicationCategoryType::MEDIA},
+            {"shared:vendor.package.C", ApplicationCategoryType::MEDIA},
+            {"vendor.package.shared.uid.D", ApplicationCategoryType::MAPS},
     };
     peer.setPackageConfigurations({"vendor.pkg"}, packagesToAppCategories);
     /*
@@ -213,23 +202,38 @@
                                   ApplicationCategoryType::OTHERS)},
             {15100,
              constructPackageInfo("vendor.package.A", 15100, UidType::APPLICATION,
-                                  ComponentType::VENDOR, ApplicationCategoryType::MEDIA)},
+                                  ComponentType::VENDOR, ApplicationCategoryType::OTHERS)},
             {16700,
              constructPackageInfo("vendor.pkg", 16700, UidType::NATIVE, ComponentType::VENDOR,
                                   ApplicationCategoryType::OTHERS)},
+            {18100,
+             constructPackageInfo("shared:vendor.package.C", 18100, UidType::APPLICATION,
+                                  ComponentType::VENDOR, ApplicationCategoryType::OTHERS)},
+            {19100,
+             constructPackageInfo("shared:vendor.package.D", 19100, UidType::APPLICATION,
+                                  ComponentType::VENDOR, ApplicationCategoryType::OTHERS,
+                                  {"vendor.package.shared.uid.D"})},
     };
 
-    std::vector<int32_t> expectedUids = {6100, 7700, 15100, 16700};
+    std::vector<int32_t> expectedUids = {6100, 7700, 15100, 16700, 18100, 19100};
     std::vector<std::string> expectedPrefixes = {"vendor.pkg"};
     std::vector<PackageInfo> injectPackageInfos = {expectedMappings.at(6100),
                                                    expectedMappings.at(7700),
                                                    expectedMappings.at(15100),
-                                                   expectedMappings.at(16700)};
+                                                   expectedMappings.at(16700),
+                                                   expectedMappings.at(18100),
+                                                   expectedMappings.at(19100)};
+
+    expectedMappings.at(15100).appCategoryType = ApplicationCategoryType::MEDIA;
+    expectedMappings.at(18100).appCategoryType = ApplicationCategoryType::MEDIA;
+    expectedMappings.at(19100).appCategoryType = ApplicationCategoryType::MAPS;
+
     EXPECT_CALL(*peer.mockWatchdogServiceHelper,
                 getPackageInfosForUids(expectedUids, expectedPrefixes, _))
             .WillOnce(DoAll(SetArgPointee<2>(injectPackageInfos), Return(binder::Status::ok())));
 
-    auto actualMappings = packageInfoResolver->getPackageInfosForUids({6100, 7700, 15100, 16700});
+    auto actualMappings =
+            packageInfoResolver->getPackageInfosForUids({6100, 7700, 15100, 16700, 18100, 19100});
 
     EXPECT_THAT(actualMappings, UnorderedElementsAreArray(expectedMappings))
             << "Expected: " << toString(expectedMappings)
diff --git a/cpp/watchdog/server/tests/PackageInfoTestUtils.cpp b/cpp/watchdog/server/tests/PackageInfoTestUtils.cpp
new file mode 100644
index 0000000..0834b2c
--- /dev/null
+++ b/cpp/watchdog/server/tests/PackageInfoTestUtils.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+#include "PackageInfoTestUtils.h"
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::automotive::watchdog::internal::ApplicationCategoryType;
+using ::android::automotive::watchdog::internal::ComponentType;
+using ::android::automotive::watchdog::internal::PackageInfo;
+using ::android::automotive::watchdog::internal::UidType;
+
+PackageInfo constructPackageInfo(const char* packageName, int32_t uid, UidType uidType,
+                                 ComponentType componentType,
+                                 ApplicationCategoryType appCategoryType,
+                                 std::vector<std::string> sharedUidPackages) {
+    PackageInfo packageInfo;
+    packageInfo.packageIdentifier.name = packageName;
+    packageInfo.packageIdentifier.uid = uid;
+    packageInfo.uidType = uidType;
+    packageInfo.componentType = componentType;
+    packageInfo.appCategoryType = appCategoryType;
+    packageInfo.sharedUidPackages = sharedUidPackages;
+    return packageInfo;
+}
+
+PackageInfo constructAppPackageInfo(const char* packageName, const ComponentType componentType,
+                                    const ApplicationCategoryType appCategoryType,
+                                    const std::vector<std::string>& sharedUidPackages) {
+    return constructPackageInfo(packageName, 0, UidType::APPLICATION, componentType,
+                                appCategoryType, sharedUidPackages);
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/watchdog/server/tests/PackageInfoTestUtils.h b/cpp/watchdog/server/tests/PackageInfoTestUtils.h
new file mode 100644
index 0000000..fc38434
--- /dev/null
+++ b/cpp/watchdog/server/tests/PackageInfoTestUtils.h
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_PACKAGEINFOTESTUTILS_H_
+#define CPP_WATCHDOG_SERVER_TESTS_PACKAGEINFOTESTUTILS_H_
+
+#include <android/automotive/watchdog/internal/ApplicationCategoryType.h>
+#include <android/automotive/watchdog/internal/ComponentType.h>
+#include <android/automotive/watchdog/internal/PackageInfo.h>
+#include <android/automotive/watchdog/internal/UidType.h>
+#include <gmock/gmock.h>
+
+#include <string>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+android::automotive::watchdog::internal::PackageInfo constructPackageInfo(
+        const char* packageName, int32_t uid,
+        android::automotive::watchdog::internal::UidType uidType =
+                android::automotive::watchdog::internal::UidType::NATIVE,
+        android::automotive::watchdog::internal::ComponentType componentType =
+                android::automotive::watchdog::internal::ComponentType::UNKNOWN,
+        android::automotive::watchdog::internal::ApplicationCategoryType appCategoryType =
+                android::automotive::watchdog::internal::ApplicationCategoryType::OTHERS,
+        std::vector<std::string> sharedUidPackages = {});
+
+android::automotive::watchdog::internal::PackageInfo constructAppPackageInfo(
+        const char* packageName,
+        const android::automotive::watchdog::internal::ComponentType componentType,
+        const android::automotive::watchdog::internal::ApplicationCategoryType appCategoryType =
+                android::automotive::watchdog::internal::ApplicationCategoryType::OTHERS,
+        const std::vector<std::string>& sharedUidPackages = {});
+
+MATCHER_P(PackageIdentifierEq, expected, "") {
+    const auto& actual = arg;
+    return ::testing::Value(actual.name, ::testing::Eq(expected.name)) &&
+            ::testing::Value(actual.uid, ::testing::Eq(expected.uid));
+}
+
+MATCHER_P(PackageInfoEq, expected, "") {
+    const auto& actual = arg;
+    return ::testing::Value(actual.packageIdentifier,
+                            PackageIdentifierEq(expected.packageIdentifier)) &&
+            ::testing::Value(actual.uidType, ::testing::Eq(expected.uidType)) &&
+            ::testing::Value(actual.sharedUidPackages,
+                             ::testing::UnorderedElementsAreArray(expected.sharedUidPackages)) &&
+            ::testing::Value(actual.componentType, ::testing::Eq(expected.componentType)) &&
+            ::testing::Value(actual.appCategoryType, ::testing::Eq(expected.appCategoryType));
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
+
+#endif  // CPP_WATCHDOG_SERVER_TESTS_PACKAGEINFOTESTUTILS_H_
diff --git a/cpp/watchdog/server/tests/ProcPidDir.cpp b/cpp/watchdog/server/tests/ProcPidDir.cpp
index 134d231..b6662d7 100644
--- a/cpp/watchdog/server/tests/ProcPidDir.cpp
+++ b/cpp/watchdog/server/tests/ProcPidDir.cpp
@@ -16,10 +16,11 @@
 
 #include "ProcPidDir.h"
 
-#include "ProcPidStat.h"
+#include "UidProcStatsCollector.h"
 
 #include <android-base/file.h>
 #include <android-base/result.h>
+
 #include <errno.h>
 
 namespace android {
diff --git a/cpp/watchdog/server/tests/ProcPidStatTest.cpp b/cpp/watchdog/server/tests/ProcPidStatTest.cpp
deleted file mode 100644
index 8c3afbc..0000000
--- a/cpp/watchdog/server/tests/ProcPidStatTest.cpp
+++ /dev/null
@@ -1,553 +0,0 @@
-/*
- * Copyright 2020 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.
- */
-
-#include "ProcPidStat.h"
-
-#include "ProcPidDir.h"
-
-#include <android-base/file.h>
-#include <android-base/stringprintf.h>
-#include <gmock/gmock.h>
-#include <inttypes.h>
-
-#include <algorithm>
-#include <string>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::automotive::watchdog::testing::populateProcPidDir;
-using ::android::base::StringAppendF;
-using ::android::base::StringPrintf;
-
-namespace {
-
-std::string toString(const PidStat& stat) {
-    return StringPrintf("PID: %" PRIu32 ", PPID: %" PRIu32 ", Comm: %s, State: %s, "
-                        "Major page faults: %" PRIu64 ", Num threads: %" PRIu32
-                        ", Start time: %" PRIu64,
-                        stat.pid, stat.ppid, stat.comm.c_str(), stat.state.c_str(),
-                        stat.majorFaults, stat.numThreads, stat.startTime);
-}
-
-std::string toString(const ProcessStats& stats) {
-    std::string buffer;
-    StringAppendF(&buffer,
-                  "Tgid: %" PRIi64 ", UID: %" PRIi64 ", VmPeak: %" PRIu64 ", VmSize: %" PRIu64
-                  ", VmHWM: %" PRIu64 ", VmRSS: %" PRIu64 ", %s\n",
-                  stats.tgid, stats.uid, stats.vmPeakKb, stats.vmSizeKb, stats.vmHwmKb,
-                  stats.vmRssKb, toString(stats.process).c_str());
-    StringAppendF(&buffer, "\tThread stats:\n");
-    for (const auto& it : stats.threads) {
-        StringAppendF(&buffer, "\t\t%s\n", toString(it.second).c_str());
-    }
-    StringAppendF(&buffer, "\n");
-    return buffer;
-}
-
-std::string toString(const std::vector<ProcessStats>& stats) {
-    std::string buffer;
-    StringAppendF(&buffer, "Number of processes: %d\n", static_cast<int>(stats.size()));
-    for (const auto& it : stats) {
-        StringAppendF(&buffer, "%s", toString(it).c_str());
-    }
-    return buffer;
-}
-
-bool isEqual(const PidStat& lhs, const PidStat& rhs) {
-    return lhs.pid == rhs.pid && lhs.comm == rhs.comm && lhs.state == rhs.state &&
-            lhs.ppid == rhs.ppid && lhs.majorFaults == rhs.majorFaults &&
-            lhs.numThreads == rhs.numThreads && lhs.startTime == rhs.startTime;
-}
-
-bool isEqual(std::vector<ProcessStats>* lhs, std::vector<ProcessStats>* rhs) {
-    if (lhs->size() != rhs->size()) {
-        return false;
-    }
-    std::sort(lhs->begin(), lhs->end(), [&](const ProcessStats& l, const ProcessStats& r) -> bool {
-        return l.process.pid < r.process.pid;
-    });
-    std::sort(rhs->begin(), rhs->end(), [&](const ProcessStats& l, const ProcessStats& r) -> bool {
-        return l.process.pid < r.process.pid;
-    });
-    return std::equal(lhs->begin(), lhs->end(), rhs->begin(),
-                      [&](const ProcessStats& l, const ProcessStats& r) -> bool {
-                          if (l.tgid != r.tgid || l.uid != r.uid || l.vmPeakKb != r.vmPeakKb ||
-                              l.vmSizeKb != r.vmSizeKb || l.vmHwmKb != r.vmHwmKb ||
-                              l.vmRssKb != r.vmRssKb || !isEqual(l.process, r.process) ||
-                              l.threads.size() != r.threads.size()) {
-                              return false;
-                          }
-                          for (const auto& lIt : l.threads) {
-                              const auto& rIt = r.threads.find(lIt.first);
-                              if (rIt == r.threads.end()) {
-                                  return false;
-                              }
-                              if (!isEqual(lIt.second, rIt->second)) {
-                                  return false;
-                              }
-                          }
-                          return true;
-                      });
-}
-
-std::string pidStatusStr(pid_t pid, uid_t uid) {
-    return StringPrintf("Pid:\t%" PRIu32 "\nTgid:\t%" PRIu32 "\nUid:\t%" PRIu32 "\n", pid, pid,
-                        uid);
-}
-
-std::string pidStatusStr(pid_t pid, uid_t uid, uint64_t vmPeakKb, uint64_t vmSizeKb,
-                         uint64_t vmHwmKb, uint64_t vmRssKb) {
-    return StringPrintf("%sVmPeak:\t%" PRIu64 "\nVmSize:\t%" PRIu64 "\nVmHWM:\t%" PRIu64
-                        "\nVmRSS:\t%" PRIu64 "\n",
-                        pidStatusStr(pid, uid).c_str(), vmPeakKb, vmSizeKb, vmHwmKb, vmRssKb);
-}
-
-}  // namespace
-
-TEST(ProcPidStatTest, TestValidStatFiles) {
-    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
-            {1, {1, 453}},
-            {1000, {1000, 1100}},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0\n"},
-            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 1000\n"},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStatus = {
-            {1, pidStatusStr(1, 0, 123, 456, 789, 345)},
-            {1000, pidStatusStr(1000, 10001234, 234, 567, 890, 123)},
-    };
-
-    std::unordered_map<pid_t, std::string> perThreadStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 0\n"},
-            {453, "453 (init) S 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 0 2 0 275\n"},
-            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 2 0 1000\n"},
-            {1100, "1100 (system_server) S 1 0 0 0 0 0 0 0 350 0 0 0 0 0 0 0 2 0 1200\n"},
-    };
-
-    std::vector<ProcessStats> expected = {
-            {.tgid = 1,
-             .uid = 0,
-             .vmPeakKb = 123,
-             .vmSizeKb = 456,
-             .vmHwmKb = 789,
-             .vmRssKb = 345,
-             .process = {1, "init", "S", 0, 220, 2, 0},
-             .threads = {{1, {1, "init", "S", 0, 200, 2, 0}},
-                         {453, {453, "init", "S", 0, 20, 2, 275}}}},
-            {.tgid = 1000,
-             .uid = 10001234,
-             .vmPeakKb = 234,
-             .vmSizeKb = 567,
-             .vmHwmKb = 890,
-             .vmRssKb = 123,
-             .process = {1000, "system_server", "R", 1, 600, 2, 1000},
-             .threads = {{1000, {1000, "system_server", "R", 1, 250, 2, 1000}},
-                         {1100, {1100, "system_server", "S", 1, 350, 2, 1200}}}},
-    };
-
-    TemporaryDir firstSnapshot;
-    ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
-                                        perProcessStatus, perThreadStat));
-
-    ProcPidStat procPidStat(firstSnapshot.path);
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
-    ASSERT_RESULT_OK(procPidStat.collect());
-
-    auto actual = std::vector<ProcessStats>(procPidStat.deltaStats());
-    EXPECT_TRUE(isEqual(&expected, &actual)) << "First snapshot doesn't match.\nExpected:\n"
-                                             << toString(expected) << "\nActual:\n"
-                                             << toString(actual);
-    pidToTids = {
-            {1, {1, 453}}, {1000, {1000, 1400}},  // TID 1100 terminated and 1400 instantiated.
-    };
-
-    perProcessStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 920 0 0 0 0 0 0 0 2 0 0\n"},
-            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 1550 0 0 0 0 0 0 0 2 0 1000\n"},
-    };
-
-    perThreadStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 0\n"},
-            {453, "453 (init) S 0 0 0 0 0 0 0 0 320 0 0 0 0 0 0 0 2 0 275\n"},
-            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 1000\n"},
-            // TID 1100 hits +400 major page faults before terminating. This is counted against
-            // PID 1000's perProcessStat.
-            {1400, "1400 (system_server) S 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 8977476\n"},
-    };
-
-    expected = {
-            {.tgid = 1,
-             .uid = 0,
-             .vmPeakKb = 123,
-             .vmSizeKb = 456,
-             .vmHwmKb = 789,
-             .vmRssKb = 345,
-             .process = {1, "init", "S", 0, 700, 2, 0},
-             .threads = {{1, {1, "init", "S", 0, 400, 2, 0}},
-                         {453, {453, "init", "S", 0, 300, 2, 275}}}},
-            {.tgid = 1000,
-             .uid = 10001234,
-             .vmPeakKb = 234,
-             .vmSizeKb = 567,
-             .vmHwmKb = 890,
-             .vmRssKb = 123,
-             .process = {1000, "system_server", "R", 1, 950, 2, 1000},
-             .threads = {{1000, {1000, "system_server", "R", 1, 350, 2, 1000}},
-                         {1400, {1400, "system_server", "S", 1, 200, 2, 8977476}}}},
-    };
-
-    TemporaryDir secondSnapshot;
-    ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
-                                        perProcessStatus, perThreadStat));
-
-    procPidStat.mPath = secondSnapshot.path;
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
-    ASSERT_RESULT_OK(procPidStat.collect());
-
-    actual = std::vector<ProcessStats>(procPidStat.deltaStats());
-    EXPECT_TRUE(isEqual(&expected, &actual)) << "Second snapshot doesn't match.\nExpected:\n"
-                                             << toString(expected) << "\nActual:\n"
-                                             << toString(actual);
-}
-
-TEST(ProcPidStatTest, TestHandlesProcessTerminationBetweenScanningAndParsing) {
-    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
-            {1, {1}},
-            {100, {100}},          // Process terminates after scanning PID directory.
-            {1000, {1000}},        // Process terminates after reading stat file.
-            {2000, {2000}},        // Process terminates after scanning task directory.
-            {3000, {3000, 3300}},  // TID 3300 terminates after scanning task directory.
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 1 0 0\n"},
-            // Process 100 terminated.
-            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 1 0 1000\n"},
-            {2000, "2000 (logd) R 1 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 1 0 4567\n"},
-            {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 10300 0 0 0 0 0 0 0 2 0 67890\n"},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStatus = {
-            {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
-            // Process 1000 terminated.
-            {2000, pidStatusStr(2000, 10001234)},
-            {3000, pidStatusStr(3000, 10001234)},
-    };
-
-    std::unordered_map<pid_t, std::string> perThreadStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
-            // Process 2000 terminated.
-            {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 2400 0 0 0 0 0 0 0 2 0 67890\n"},
-            // TID 3300 terminated.
-    };
-
-    std::vector<ProcessStats> expected = {
-            {.tgid = 1,
-             .uid = 0,
-             .process = {1, "init", "S", 0, 220, 1, 0},
-             .threads = {{1, {1, "init", "S", 0, 200, 1, 0}}}},
-            {.tgid = -1,
-             .uid = -1,
-             .process = {1000, "system_server", "R", 1, 600, 1, 1000},
-             // Stats common between process and main-thread are copied when
-             // main-thread stats are not available.
-             .threads = {{1000, {1000, "system_server", "R", 1, 0, 1, 1000}}}},
-            {.tgid = 2000,
-             .uid = 10001234,
-             .process = {2000, "logd", "R", 1, 1200, 1, 4567},
-             .threads = {{2000, {2000, "logd", "R", 1, 0, 1, 4567}}}},
-            {.tgid = 3000,
-             .uid = 10001234,
-             .process = {3000, "disk I/O", "R", 1, 10300, 2, 67890},
-             .threads = {{3000, {3000, "disk I/O", "R", 1, 2400, 2, 67890}}}},
-    };
-
-    TemporaryDir procDir;
-    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
-                                        perThreadStat));
-
-    ProcPidStat procPidStat(procDir.path);
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << procDir.path << "` are inaccessible";
-    ASSERT_RESULT_OK(procPidStat.collect());
-
-    auto actual = std::vector<ProcessStats>(procPidStat.deltaStats());
-    EXPECT_TRUE(isEqual(&expected, &actual)) << "Proc pid contents doesn't match.\nExpected:\n"
-                                             << toString(expected) << "\nActual:\n"
-                                             << toString(actual);
-}
-
-TEST(ProcPidStatTest, TestHandlesPidTidReuse) {
-    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
-            {1, {1, 367, 453, 589}},
-            {1000, {1000}},
-            {2345, {2345}},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 4 0 0\n"},
-            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
-            {2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStatus = {
-            {1, pidStatusStr(1, 0)},
-            {1000, pidStatusStr(1000, 10001234)},
-            {2345, pidStatusStr(2345, 10001234)},
-    };
-
-    std::unordered_map<pid_t, std::string> perThreadStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 4 0 0\n"},
-            {367, "367 (init) S 0 0 0 0 0 0 0 0 400 0 0 0 0 0 0 0 4 0 100\n"},
-            {453, "453 (init) S 0 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 4 0 275\n"},
-            {589, "589 (init) S 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 4 0 600\n"},
-            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
-            {2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
-    };
-
-    std::vector<ProcessStats> expected = {
-            {.tgid = 1,
-             .uid = 0,
-             .process = {1, "init", "S", 0, 1200, 4, 0},
-             .threads = {{1, {1, "init", "S", 0, 200, 4, 0}},
-                         {367, {367, "init", "S", 0, 400, 4, 100}},
-                         {453, {453, "init", "S", 0, 100, 4, 275}},
-                         {589, {589, "init", "S", 0, 500, 4, 600}}}},
-            {.tgid = 1000,
-             .uid = 10001234,
-             .process = {1000, "system_server", "R", 1, 250, 1, 1000},
-             .threads = {{1000, {1000, "system_server", "R", 1, 250, 1, 1000}}}},
-            {.tgid = 2345,
-             .uid = 10001234,
-             .process = {2345, "logd", "R", 1, 54354, 1, 456},
-             .threads = {{2345, {2345, "logd", "R", 1, 54354, 1, 456}}}},
-    };
-
-    TemporaryDir firstSnapshot;
-    ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
-                                        perProcessStatus, perThreadStat));
-
-    ProcPidStat procPidStat(firstSnapshot.path);
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
-    ASSERT_RESULT_OK(procPidStat.collect());
-
-    auto actual = std::vector<ProcessStats>(procPidStat.deltaStats());
-    EXPECT_TRUE(isEqual(&expected, &actual)) << "First snapshot doesn't match.\nExpected:\n"
-                                             << toString(expected) << "\nActual:\n"
-                                             << toString(actual);
-
-    pidToTids = {
-            {1, {1, 589}},       // TID 589 reused by the same process.
-            {367, {367, 2000}},  // TID 367 reused as a PID. PID 2000 reused as a TID.
-            // PID 1000 reused as a new PID. TID 453 reused by a different PID.
-            {1000, {1000, 453}},
-    };
-
-    perProcessStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 0\n"},
-            {367, "367 (system_server) R 1 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 2 0 3450\n"},
-            {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 2000 0 0 0 0 0 0 0 2 0 4650\n"},
-    };
-
-    perProcessStatus = {
-            {1, pidStatusStr(1, 0)},
-            {367, pidStatusStr(367, 10001234)},
-            {1000, pidStatusStr(1000, 10001234)},
-    };
-
-    perThreadStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 2 0 0\n"},
-            {589, "589 (init) S 0 0 0 0 0 0 0 0 300 0 0 0 0 0 0 0 2 0 2345\n"},
-            {367, "367 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3450\n"},
-            {2000, "2000 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3670\n"},
-            {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 4650\n"},
-            {453, "453 (logd) D 1 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 4770\n"},
-    };
-
-    expected = {
-            {.tgid = 1,
-             .uid = 0,
-             .process = {1, "init", "S", 0, 600, 2, 0},
-             .threads = {{1, {1, "init", "S", 0, 300, 2, 0}},
-                         {589, {589, "init", "S", 0, 300, 2, 2345}}}},
-            {.tgid = 367,
-             .uid = 10001234,
-             .process = {367, "system_server", "R", 1, 100, 2, 3450},
-             .threads = {{367, {367, "system_server", "R", 1, 50, 2, 3450}},
-                         {2000, {2000, "system_server", "R", 1, 50, 2, 3670}}}},
-            {.tgid = 1000,
-             .uid = 10001234,
-             .process = {1000, "logd", "R", 1, 2000, 2, 4650},
-             .threads = {{1000, {1000, "logd", "R", 1, 200, 2, 4650}},
-                         {453, {453, "logd", "D", 1, 1800, 2, 4770}}}},
-    };
-
-    TemporaryDir secondSnapshot;
-    ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
-                                        perProcessStatus, perThreadStat));
-
-    procPidStat.mPath = secondSnapshot.path;
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
-    ASSERT_RESULT_OK(procPidStat.collect());
-
-    actual = std::vector<ProcessStats>(procPidStat.deltaStats());
-    EXPECT_TRUE(isEqual(&expected, &actual)) << "Second snapshot doesn't match.\nExpected:\n"
-                                             << toString(expected) << "\nActual:\n"
-                                             << toString(actual);
-}
-
-TEST(ProcPidStatTest, TestErrorOnCorruptedProcessStatFile) {
-    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
-            {1, {1}},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStatus = {
-            {1, pidStatusStr(1, 0)},
-    };
-
-    std::unordered_map<pid_t, std::string> perThreadStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
-    };
-
-    TemporaryDir procDir;
-    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
-                                        perThreadStat));
-
-    ProcPidStat procPidStat(procDir.path);
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << procDir.path << "` are inaccessible";
-    ASSERT_FALSE(procPidStat.collect().ok()) << "No error returned for invalid process stat file";
-}
-
-TEST(ProcPidStatTest, TestErrorOnCorruptedProcessStatusFile) {
-    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
-            {1, {1}},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStatus = {
-            {1, "Pid:\t1\nTgid:\t1\nCORRUPTED DATA\n"},
-    };
-
-    std::unordered_map<pid_t, std::string> perThreadStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
-    };
-
-    TemporaryDir procDir;
-    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
-                                        perThreadStat));
-
-    ProcPidStat procPidStat(procDir.path);
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << procDir.path << "` are inaccessible";
-    ASSERT_FALSE(procPidStat.collect().ok()) << "No error returned for invalid process status file";
-}
-
-TEST(ProcPidStatTest, TestErrorOnCorruptedThreadStatFile) {
-    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
-            {1, {1}},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStatus = {
-            {1, pidStatusStr(1, 0)},
-    };
-
-    std::unordered_map<pid_t, std::string> perThreadStat = {
-            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
-    };
-
-    TemporaryDir procDir;
-    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
-                                        perThreadStat));
-
-    ProcPidStat procPidStat(procDir.path);
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << procDir.path << "` are inaccessible";
-    ASSERT_FALSE(procPidStat.collect().ok()) << "No error returned for invalid thread stat file";
-}
-
-TEST(ProcPidStatTest, TestHandlesSpaceInCommName) {
-    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
-            {1, {1}},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStat = {
-            {1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
-    };
-
-    std::unordered_map<pid_t, std::string> perProcessStatus = {
-            {1, pidStatusStr(1, 0)},
-    };
-
-    std::unordered_map<pid_t, std::string> perThreadStat = {
-            {1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
-    };
-
-    std::vector<ProcessStats> expected = {
-            {.tgid = 1,
-             .uid = 0,
-             .process = {1, "random process name with space", "S", 0, 200, 1, 0},
-             .threads = {{1, {1, "random process name with space", "S", 0, 200, 1, 0}}}},
-    };
-
-    TemporaryDir procDir;
-    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
-                                        perThreadStat));
-
-    ProcPidStat procPidStat(procDir.path);
-    ASSERT_TRUE(procPidStat.enabled())
-            << "Files under the path `" << procDir.path << "` are inaccessible";
-    ASSERT_RESULT_OK(procPidStat.collect());
-
-    auto actual = std::vector<ProcessStats>(procPidStat.deltaStats());
-    EXPECT_TRUE(isEqual(&expected, &actual)) << "Proc pid contents doesn't match.\nExpected:\n"
-                                             << toString(expected) << "\nActual:\n"
-                                             << toString(actual);
-}
-
-TEST(ProcPidStatTest, TestProcPidStatContentsFromDevice) {
-    ProcPidStat procPidStat;
-    ASSERT_TRUE(procPidStat.enabled()) << "/proc/[pid]/.* files are inaccessible";
-    ASSERT_RESULT_OK(procPidStat.collect());
-
-    const auto& processStats = procPidStat.deltaStats();
-    // The below check should pass because there should be at least one process.
-    EXPECT_GT(processStats.size(), 0);
-}
-
-}  // namespace watchdog
-}  // namespace automotive
-}  // namespace android
diff --git a/cpp/watchdog/server/tests/ProcStatTest.cpp b/cpp/watchdog/server/tests/ProcStatTest.cpp
index c886310..ee33162 100644
--- a/cpp/watchdog/server/tests/ProcStatTest.cpp
+++ b/cpp/watchdog/server/tests/ProcStatTest.cpp
@@ -42,7 +42,7 @@
                         cpuStats.userTime, cpuStats.niceTime, cpuStats.sysTime, cpuStats.idleTime,
                         cpuStats.ioWaitTime, cpuStats.irqTime, cpuStats.softIrqTime,
                         cpuStats.stealTime, cpuStats.guestTime, cpuStats.guestNiceTime,
-                        info.runnableProcessesCnt, info.ioBlockedProcessesCnt);
+                        info.runnableProcessCount, info.ioBlockedProcessCount);
 }
 
 }  // namespace
@@ -56,8 +56,9 @@
             "cpu3 1000 20 190 650 109 130 140 0 0 0\n"
             "intr 694351583 0 0 0 297062868 0 5922464 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "
             "0 0\n"
-            // Skipped most of the intr line as it is not important for testing the ProcStat parsing
-            // logic.
+            /* Skipped most of the intr line as it is not important for testing the ProcStat parsing
+             * logic.
+             */
             "ctxt 579020168\n"
             "btime 1579718450\n"
             "processes 113804\n"
@@ -77,8 +78,8 @@
             .guestTime = 0,
             .guestNiceTime = 0,
     };
-    expectedFirstDelta.runnableProcessesCnt = 17;
-    expectedFirstDelta.ioBlockedProcessesCnt = 5;
+    expectedFirstDelta.runnableProcessCount = 17;
+    expectedFirstDelta.ioBlockedProcessCount = 5;
 
     TemporaryFile tf;
     ASSERT_NE(tf.fd, -1);
@@ -120,8 +121,8 @@
             .guestTime = 0,
             .guestNiceTime = 0,
     };
-    expectedSecondDelta.runnableProcessesCnt = 10;
-    expectedSecondDelta.ioBlockedProcessesCnt = 2;
+    expectedSecondDelta.runnableProcessCount = 10;
+    expectedSecondDelta.ioBlockedProcessCount = 2;
 
     ASSERT_TRUE(WriteStringToFile(secondSnapshot, tf.path));
     ASSERT_RESULT_OK(procStat.collect());
@@ -257,10 +258,11 @@
     ASSERT_RESULT_OK(procStat.collect());
 
     const auto& info = procStat.deltaStats();
-    // The below checks should pass because the /proc/stats file should have the CPU time spent
-    // since bootup and there should be at least one running process.
+    /* The below checks should pass because the /proc/stats file should have the CPU time spent
+     * since bootup and there should be at least one running process.
+     */
     EXPECT_GT(info.totalCpuTime(), 0);
-    EXPECT_GT(info.totalProcessesCnt(), 0);
+    EXPECT_GT(info.totalProcessCount(), 0);
 }
 
 }  // namespace watchdog
diff --git a/cpp/watchdog/server/tests/UidIoStatsCollectorTest.cpp b/cpp/watchdog/server/tests/UidIoStatsCollectorTest.cpp
new file mode 100644
index 0000000..fe30844
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidIoStatsCollectorTest.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020 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.
+ */
+
+#include "UidIoStatsCollector.h"
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <gmock/gmock.h>
+
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::base::StringAppendF;
+using ::android::base::WriteStringToFile;
+using ::testing::UnorderedElementsAreArray;
+
+namespace {
+
+std::string toString(std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid) {
+    std::string buffer;
+    for (const auto& [uid, stats] : uidIoStatsByUid) {
+        StringAppendF(&buffer, "{%d: %s}\n", uid, stats.toString().c_str());
+    }
+    return buffer;
+}
+
+}  // namespace
+
+TEST(UidIoStatsCollectorTest, TestValidStatFile) {
+    // Format: uid fgRdChar fgWrChar fgRdBytes fgWrBytes bgRdChar bgWrChar bgRdBytes bgWrBytes
+    // fgFsync bgFsync
+    constexpr char firstSnapshot[] = "1001234 5000 1000 3000 500 0 0 0 0 20 0\n"
+                                     "1005678 500 100 30 50 300 400 100 200 45 60\n"
+                                     "1009 0 0 0 0 40000 50000 20000 30000 0 300\n"
+                                     "1001000 4000 3000 2000 1000 400 300 200 100 50 10\n";
+    std::unordered_map<uid_t, UidIoStats> expectedFirstUsage =
+            {{1001234,
+              UidIoStats{/*fgRdBytes=*/3'000, /*bgRdBytes=*/0, /*fgWrBytes=*/500,
+                         /*bgWrBytes=*/0, /*fgFsync=*/20, /*bgFsync=*/0}},
+             {1005678,
+              UidIoStats{/*fgRdBytes=*/30, /*bgRdBytes=*/100, /*fgWrBytes=*/50, /*bgWrBytes=*/200,
+                         /*fgFsync=*/45, /*bgFsync=*/60}},
+             {1009,
+              UidIoStats{/*fgRdBytes=*/0, /*bgRdBytes=*/20'000, /*fgWrBytes=*/0,
+                         /*bgWrBytes=*/30'000,
+                         /*fgFsync=*/0, /*bgFsync=*/300}},
+             {1001000,
+              UidIoStats{/*fgRdBytes=*/2'000, /*bgRdBytes=*/200, /*fgWrBytes=*/1'000,
+                         /*bgWrBytes=*/100, /*fgFsync=*/50, /*bgFsync=*/10}}};
+    TemporaryFile tf;
+    ASSERT_NE(tf.fd, -1);
+    ASSERT_TRUE(WriteStringToFile(firstSnapshot, tf.path));
+
+    UidIoStatsCollector collector(tf.path);
+    ASSERT_TRUE(collector.enabled()) << "Temporary file is inaccessible";
+    ASSERT_RESULT_OK(collector.collect());
+
+    const auto& actualFirstUsage = collector.deltaStats();
+    EXPECT_THAT(actualFirstUsage, UnorderedElementsAreArray(expectedFirstUsage))
+            << "Expected: " << toString(expectedFirstUsage)
+            << "Actual: " << toString(actualFirstUsage);
+
+    constexpr char secondSnapshot[] = "1001234 10000 2000 7000 950 0 0 0 0 45 0\n"
+                                      "1005678 600 100 40 50 1000 1000 1000 600 50 70\n"
+                                      "1003456 300 500 200 300 0 0 0 0 50 0\n"
+                                      "1001000 400 300 200 100 40 30 20 10 5 1\n";
+    std::unordered_map<uid_t, UidIoStats> expectedSecondUsage =
+            {{1001234,
+              UidIoStats{/*fgRdBytes=*/4'000, /*bgRdBytes=*/0,
+                         /*fgWrBytes=*/450, /*bgWrBytes=*/0, /*fgFsync=*/25,
+                         /*bgFsync=*/0}},
+             {1005678,
+              UidIoStats{/*fgRdBytes=*/10, /*bgRdBytes=*/900, /*fgWrBytes=*/0, /*bgWrBytes=*/400,
+                         /*fgFsync=*/5, /*bgFsync=*/10}},
+             {1003456,
+              UidIoStats{/*fgRdBytes=*/200, /*bgRdBytes=*/0, /*fgWrBytes=*/300, /*bgWrBytes=*/0,
+                         /*fgFsync=*/50, /*bgFsync=*/0}}};
+    ASSERT_TRUE(WriteStringToFile(secondSnapshot, tf.path));
+    ASSERT_RESULT_OK(collector.collect());
+
+    const auto& actualSecondUsage = collector.deltaStats();
+    EXPECT_THAT(actualSecondUsage, UnorderedElementsAreArray(expectedSecondUsage))
+            << "Expected: " << toString(expectedSecondUsage)
+            << "Actual: " << toString(actualSecondUsage);
+}
+
+TEST(UidIoStatsCollectorTest, TestErrorOnInvalidStatFile) {
+    // Format: uid fgRdChar fgWrChar fgRdBytes fgWrBytes bgRdChar bgWrChar bgRdBytes bgWrBytes
+    // fgFsync bgFsync
+    constexpr char contents[] = "1001234 5000 1000 3000 500 0 0 0 0 20 0\n"
+                                "1005678 500 100 30 50 300 400 100 200 45 60\n"
+                                "1009012 0 0 0 0 40000 50000 20000 30000 0 300\n"
+                                "1001000 4000 3000 2000 1000 CORRUPTED DATA\n";
+    TemporaryFile tf;
+    ASSERT_NE(tf.fd, -1);
+    ASSERT_TRUE(WriteStringToFile(contents, tf.path));
+
+    UidIoStatsCollector collector(tf.path);
+    ASSERT_TRUE(collector.enabled()) << "Temporary file is inaccessible";
+    EXPECT_FALSE(collector.collect().ok()) << "No error returned for invalid file";
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/watchdog/server/tests/UidIoStatsTest.cpp b/cpp/watchdog/server/tests/UidIoStatsTest.cpp
deleted file mode 100644
index 6e88ed5..0000000
--- a/cpp/watchdog/server/tests/UidIoStatsTest.cpp
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2020 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.
- */
-
-#include "UidIoStats.h"
-
-#include <android-base/file.h>
-#include <android-base/stringprintf.h>
-#include <gmock/gmock.h>
-
-#include <unordered_map>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::base::StringAppendF;
-using ::android::base::WriteStringToFile;
-using ::testing::UnorderedElementsAreArray;
-
-namespace {
-
-std::string toString(std::unordered_map<uid_t, UidIoUsage> usages) {
-    std::string buffer;
-    for (const auto& [uid, usage] : usages) {
-        StringAppendF(&buffer, "{%s}\n", usage.toString().c_str());
-    }
-    return buffer;
-}
-
-}  // namespace
-
-TEST(UidIoStatsTest, TestValidStatFile) {
-    // Format: uid fgRdChar fgWrChar fgRdBytes fgWrBytes bgRdChar bgWrChar bgRdBytes bgWrBytes
-    // fgFsync bgFsync
-    constexpr char firstSnapshot[] = "1001234 5000 1000 3000 500 0 0 0 0 20 0\n"
-                                     "1005678 500 100 30 50 300 400 100 200 45 60\n"
-                                     "1009 0 0 0 0 40000 50000 20000 30000 0 300\n"
-                                     "1001000 4000 3000 2000 1000 400 300 200 100 50 10\n";
-    std::unordered_map<uid_t, UidIoUsage> expectedFirstUsage =
-            {{1001234,
-              {.uid = 1001234,
-               .ios = {/*fgRdBytes=*/3000, /*bgRdBytes=*/0, /*fgWrBytes=*/500,
-                       /*bgWrBytes=*/0, /*fgFsync=*/20, /*bgFsync=*/0}}},
-             {1005678, {.uid = 1005678, .ios = {30, 100, 50, 200, 45, 60}}},
-             {1009, {.uid = 1009, .ios = {0, 20000, 0, 30000, 0, 300}}},
-             {1001000, {.uid = 1001000, .ios = {2000, 200, 1000, 100, 50, 10}}}};
-    TemporaryFile tf;
-    ASSERT_NE(tf.fd, -1);
-    ASSERT_TRUE(WriteStringToFile(firstSnapshot, tf.path));
-
-    UidIoStats uidIoStats(tf.path);
-    ASSERT_TRUE(uidIoStats.enabled()) << "Temporary file is inaccessible";
-    ASSERT_RESULT_OK(uidIoStats.collect());
-
-    const auto& actualFirstUsage = uidIoStats.deltaStats();
-    EXPECT_THAT(actualFirstUsage, UnorderedElementsAreArray(expectedFirstUsage))
-            << "Expected: " << toString(expectedFirstUsage)
-            << "Actual: " << toString(actualFirstUsage);
-
-    constexpr char secondSnapshot[] = "1001234 10000 2000 7000 950 0 0 0 0 45 0\n"
-                                      "1005678 600 100 40 50 1000 1000 1000 600 50 70\n"
-                                      "1003456 300 500 200 300 0 0 0 0 50 0\n"
-                                      "1001000 400 300 200 100 40 30 20 10 5 1\n";
-    std::unordered_map<uid_t, UidIoUsage> expectedSecondUsage =
-            {{1001234,
-              {.uid = 1001234,
-               .ios = {/*fgRdBytes=*/4000, /*bgRdBytes=*/0,
-                       /*fgWrBytes=*/450, /*bgWrBytes=*/0, /*fgFsync=*/25,
-                       /*bgFsync=*/0}}},
-             {1005678, {.uid = 1005678, .ios = {10, 900, 0, 400, 5, 10}}},
-             {1003456, {.uid = 1003456, .ios = {200, 0, 300, 0, 50, 0}}}};
-    ASSERT_TRUE(WriteStringToFile(secondSnapshot, tf.path));
-    ASSERT_RESULT_OK(uidIoStats.collect());
-
-    const auto& actualSecondUsage = uidIoStats.deltaStats();
-    EXPECT_THAT(actualSecondUsage, UnorderedElementsAreArray(expectedSecondUsage))
-            << "Expected: " << toString(expectedSecondUsage)
-            << "Actual: " << toString(actualSecondUsage);
-}
-
-TEST(UidIoStatsTest, TestErrorOnInvalidStatFile) {
-    // Format: uid fgRdChar fgWrChar fgRdBytes fgWrBytes bgRdChar bgWrChar bgRdBytes bgWrBytes
-    // fgFsync bgFsync
-    constexpr char contents[] = "1001234 5000 1000 3000 500 0 0 0 0 20 0\n"
-                                "1005678 500 100 30 50 300 400 100 200 45 60\n"
-                                "1009012 0 0 0 0 40000 50000 20000 30000 0 300\n"
-                                "1001000 4000 3000 2000 1000 CORRUPTED DATA\n";
-    TemporaryFile tf;
-    ASSERT_NE(tf.fd, -1);
-    ASSERT_TRUE(WriteStringToFile(contents, tf.path));
-
-    UidIoStats uidIoStats(tf.path);
-    ASSERT_TRUE(uidIoStats.enabled()) << "Temporary file is inaccessible";
-    EXPECT_FALSE(uidIoStats.collect().ok()) << "No error returned for invalid file";
-}
-
-}  // namespace watchdog
-}  // namespace automotive
-}  // namespace android
diff --git a/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp b/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp
new file mode 100644
index 0000000..c9eb6d4
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp
@@ -0,0 +1,480 @@
+/*
+ * Copyright 2020 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.
+ */
+
+#include "ProcPidDir.h"
+#include "UidProcStatsCollector.h"
+#include "UidProcStatsCollectorTestUtils.h"
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <gmock/gmock.h>
+
+#include <inttypes.h>
+
+#include <algorithm>
+#include <string>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::automotive::watchdog::testing::populateProcPidDir;
+using ::android::base::StringAppendF;
+using ::android::base::StringPrintf;
+using ::testing::UnorderedPointwise;
+
+namespace {
+
+MATCHER(UidProcStatsByUidEq, "") {
+    const auto& actual = std::get<0>(arg);
+    const auto& expected = std::get<1>(arg);
+    return actual.first == expected.first &&
+            ExplainMatchResult(UidProcStatsEq(expected.second), actual.second, result_listener);
+}
+
+std::string pidStatusStr(pid_t pid, uid_t uid) {
+    return StringPrintf("Pid:\t%" PRIu32 "\nTgid:\t%" PRIu32 "\nUid:\t%" PRIu32 "\n", pid, pid,
+                        uid);
+}
+
+std::string toString(const std::unordered_map<uid_t, UidProcStats>& uidProcStatsByUid) {
+    std::string buffer;
+    StringAppendF(&buffer, "Number of UIDs: %" PRIi32 "\n",
+                  static_cast<int>(uidProcStatsByUid.size()));
+    for (const auto& [uid, stats] : uidProcStatsByUid) {
+        StringAppendF(&buffer, "{UID: %d, %s}", uid, stats.toString().c_str());
+    }
+    return buffer;
+}
+
+}  // namespace
+
+TEST(UidProcStatsCollectorTest, TestValidStatFiles) {
+    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+            {1, {1, 453}},
+            {1000, {1000, 1100}},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0\n"},
+            {1000, "1000 (system_server) D 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 13400\n"},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStatus = {
+            {1, pidStatusStr(1, 0)},
+            {1000, pidStatusStr(1000, 10001234)},
+    };
+
+    std::unordered_map<pid_t, std::string> perThreadStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 0\n"},
+            {453, "453 (init) D 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 0 2 0 275\n"},
+            {1000, "1000 (system_server) D 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 2 0 13400\n"},
+            {1100, "1100 (system_server) D 1 0 0 0 0 0 0 0 350 0 0 0 0 0 0 0 2 0 13900\n"},
+    };
+
+    std::unordered_map<uid_t, UidProcStats> expected =
+            {{0,
+              UidProcStats{.totalMajorFaults = 220,
+                           .totalTasksCount = 2,
+                           .ioBlockedTasksCount = 1,
+                           .processStatsByPid = {{1, {"init", 0, 220, 2, 1}}}}},
+             {10001234,
+              UidProcStats{.totalMajorFaults = 600,
+                           .totalTasksCount = 2,
+                           .ioBlockedTasksCount = 2,
+                           .processStatsByPid = {{1000, {"system_server", 13'400, 600, 2, 2}}}}}};
+
+    TemporaryDir firstSnapshot;
+    ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
+                                        perProcessStatus, perThreadStat));
+
+    UidProcStatsCollector collector(firstSnapshot.path);
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
+    ASSERT_RESULT_OK(collector.collect());
+
+    auto actual = collector.deltaStats();
+
+    EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+            << "First snapshot doesn't match.\nExpected:\n"
+            << toString(expected) << "\nActual:\n"
+            << toString(actual);
+    pidToTids = {
+            {1, {1, 453}}, {1000, {1000, 1400}},  // TID 1100 terminated and 1400 instantiated.
+    };
+
+    perProcessStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 920 0 0 0 0 0 0 0 2 0 0\n"},
+            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 1550 0 0 0 0 0 0 0 2 0 13400\n"},
+    };
+
+    perThreadStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 0\n"},
+            {453, "453 (init) S 0 0 0 0 0 0 0 0 320 0 0 0 0 0 0 0 2 0 275\n"},
+            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 13400\n"},
+            // TID 1100 hits +400 major page faults before terminating. This is counted against
+            // PID 1000's perProcessStat.
+            {1400, "1400 (system_server) S 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 8977476\n"},
+    };
+
+    expected = {{0,
+                 {.totalMajorFaults = 700,
+                  .totalTasksCount = 2,
+                  .ioBlockedTasksCount = 0,
+                  .processStatsByPid = {{1, {"init", 0, 700, 2, 0}}}}},
+                {10001234,
+                 {.totalMajorFaults = 950,
+                  .totalTasksCount = 2,
+                  .ioBlockedTasksCount = 0,
+                  .processStatsByPid = {{1000, {"system_server", 13'400, 950, 2, 0}}}}}};
+
+    TemporaryDir secondSnapshot;
+    ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
+                                        perProcessStatus, perThreadStat));
+
+    collector.mPath = secondSnapshot.path;
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
+    ASSERT_RESULT_OK(collector.collect());
+
+    actual = collector.deltaStats();
+    EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+            << "Second snapshot doesn't match.\nExpected:\n"
+            << toString(expected) << "\nActual:\n"
+            << toString(actual);
+}
+
+TEST(UidProcStatsCollectorTest, TestHandlesProcessTerminationBetweenScanningAndParsing) {
+    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+            {1, {1}},
+            {100, {100}},          // Process terminates after scanning PID directory.
+            {1000, {1000}},        // Process terminates after reading stat file.
+            {2000, {2000}},        // Process terminates after scanning task directory.
+            {3000, {3000, 3300}},  // TID 3300 terminates after scanning task directory.
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 1 0 0\n"},
+            // Process 100 terminated.
+            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 1 0 1000\n"},
+            {2000, "2000 (logd) R 1 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 1 0 4567\n"},
+            {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 10300 0 0 0 0 0 0 0 2 0 67890\n"},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStatus = {
+            {1, pidStatusStr(1, 0)},
+            // Process 1000 terminated.
+            {2000, pidStatusStr(2000, 10001234)},
+            {3000, pidStatusStr(3000, 10001234)},
+    };
+
+    std::unordered_map<pid_t, std::string> perThreadStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+            // Process 2000 terminated.
+            {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 2400 0 0 0 0 0 0 0 2 0 67890\n"},
+            // TID 3300 terminated.
+    };
+
+    std::unordered_map<uid_t, UidProcStats> expected =
+            {{0,
+              UidProcStats{.totalMajorFaults = 220,
+                           .totalTasksCount = 1,
+                           .ioBlockedTasksCount = 0,
+                           .processStatsByPid = {{1, {"init", 0, 220, 1, 0}}}}},
+             {10001234,
+              UidProcStats{.totalMajorFaults = 11500,
+                           .totalTasksCount = 2,
+                           .ioBlockedTasksCount = 0,
+                           .processStatsByPid = {{2000, {"logd", 4567, 1200, 1, 0}},
+                                                 {3000, {"disk I/O", 67890, 10'300, 1, 0}}}}}};
+
+    TemporaryDir procDir;
+    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+                                        perThreadStat));
+
+    UidProcStatsCollector collector(procDir.path);
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << procDir.path << "` are inaccessible";
+    ASSERT_RESULT_OK(collector.collect());
+
+    auto actual = collector.deltaStats();
+    EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+            << "Proc pid contents doesn't match.\nExpected:\n"
+            << toString(expected) << "\nActual:\n"
+            << toString(actual);
+}
+
+TEST(UidProcStatsCollectorTest, TestHandlesPidTidReuse) {
+    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+            {1, {1, 367, 453, 589}},
+            {1000, {1000}},
+            {2345, {2345}},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 4 0 0\n"},
+            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
+            {2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStatus = {
+            {1, pidStatusStr(1, 0)},
+            {1000, pidStatusStr(1000, 10001234)},
+            {2345, pidStatusStr(2345, 10001234)},
+    };
+
+    std::unordered_map<pid_t, std::string> perThreadStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 4 0 0\n"},
+            {367, "367 (init) S 0 0 0 0 0 0 0 0 400 0 0 0 0 0 0 0 4 0 100\n"},
+            {453, "453 (init) S 0 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 4 0 275\n"},
+            {589, "589 (init) D 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 4 0 600\n"},
+            {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
+            {2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
+    };
+
+    std::unordered_map<uid_t, UidProcStats> expected =
+            {{0,
+              UidProcStats{.totalMajorFaults = 1200,
+                           .totalTasksCount = 4,
+                           .ioBlockedTasksCount = 1,
+                           .processStatsByPid = {{1, {"init", 0, 1200, 4, 1}}}}},
+             {10001234,
+              UidProcStats{.totalMajorFaults = 54'604,
+                           .totalTasksCount = 2,
+                           .ioBlockedTasksCount = 0,
+                           .processStatsByPid = {{1000, {"system_server", 1000, 250, 1, 0}},
+                                                 {2345, {"logd", 456, 54'354, 1, 0}}}}}};
+
+    TemporaryDir firstSnapshot;
+    ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
+                                        perProcessStatus, perThreadStat));
+
+    UidProcStatsCollector collector(firstSnapshot.path);
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
+    ASSERT_RESULT_OK(collector.collect());
+
+    auto actual = collector.deltaStats();
+
+    EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+            << "First snapshot doesn't match.\nExpected:\n"
+            << toString(expected) << "\nActual:\n"
+            << toString(actual);
+
+    pidToTids = {
+            {1, {1, 589}},       // TID 589 reused by the same process.
+            {367, {367, 2000}},  // TID 367 reused as a PID. PID 2000 reused as a TID.
+            // PID 1000 reused as a new PID. TID 453 reused by a different PID.
+            {1000, {1000, 453}},
+    };
+
+    perProcessStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 0\n"},
+            {367, "367 (system_server) R 1 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 2 0 3450\n"},
+            {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 2000 0 0 0 0 0 0 0 2 0 4650\n"},
+    };
+
+    perProcessStatus = {
+            {1, pidStatusStr(1, 0)},
+            {367, pidStatusStr(367, 10001234)},
+            {1000, pidStatusStr(1000, 10001234)},
+    };
+
+    perThreadStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 2 0 0\n"},
+            {589, "589 (init) S 0 0 0 0 0 0 0 0 300 0 0 0 0 0 0 0 2 0 2345\n"},
+            {367, "367 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3450\n"},
+            {2000, "2000 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3670\n"},
+            {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 4650\n"},
+            {453, "453 (logd) D 1 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 4770\n"},
+    };
+
+    expected = {{0,
+                 UidProcStats{.totalMajorFaults = 600,
+                              .totalTasksCount = 2,
+                              .ioBlockedTasksCount = 0,
+                              .processStatsByPid = {{1, {"init", 0, 600, 2, 0}}}}},
+                {10001234,
+                 UidProcStats{.totalMajorFaults = 2100,
+                              .totalTasksCount = 4,
+                              .ioBlockedTasksCount = 1,
+                              .processStatsByPid = {{367, {"system_server", 3450, 100, 2, 0}},
+                                                    {1000, {"logd", 4650, 2000, 2, 1}}}}}};
+
+    TemporaryDir secondSnapshot;
+    ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
+                                        perProcessStatus, perThreadStat));
+
+    collector.mPath = secondSnapshot.path;
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
+    ASSERT_RESULT_OK(collector.collect());
+
+    actual = collector.deltaStats();
+
+    EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+            << "Second snapshot doesn't match.\nExpected:\n"
+            << toString(expected) << "\nActual:\n"
+            << toString(actual);
+}
+
+TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedProcessStatFile) {
+    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+            {1, {1}},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStatus = {
+            {1, pidStatusStr(1, 0)},
+    };
+
+    std::unordered_map<pid_t, std::string> perThreadStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+    };
+
+    TemporaryDir procDir;
+    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+                                        perThreadStat));
+
+    UidProcStatsCollector collector(procDir.path);
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << procDir.path << "` are inaccessible";
+    ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid process stat file";
+}
+
+TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedProcessStatusFile) {
+    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+            {1, {1}},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStatus = {
+            {1, "Pid:\t1\nTgid:\t1\nCORRUPTED DATA\n"},
+    };
+
+    std::unordered_map<pid_t, std::string> perThreadStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+    };
+
+    TemporaryDir procDir;
+    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+                                        perThreadStat));
+
+    UidProcStatsCollector collector(procDir.path);
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << procDir.path << "` are inaccessible";
+    ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid process status file";
+}
+
+TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedThreadStatFile) {
+    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+            {1, {1, 234}},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 678\n"},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStatus = {
+            {1, pidStatusStr(1, 0)},
+    };
+
+    std::unordered_map<pid_t, std::string> perThreadStat = {
+            {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 678\n"},
+            {234, "234 (init) D 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
+    };
+
+    TemporaryDir procDir;
+    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+                                        perThreadStat));
+
+    UidProcStatsCollector collector(procDir.path);
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << procDir.path << "` are inaccessible";
+    ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid thread stat file";
+}
+
+TEST(UidProcStatsCollectorTest, TestHandlesSpaceInCommName) {
+    std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+            {1, {1}},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStat = {
+            {1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+    };
+
+    std::unordered_map<pid_t, std::string> perProcessStatus = {
+            {1, pidStatusStr(1, 0)},
+    };
+
+    std::unordered_map<pid_t, std::string> perThreadStat = {
+            {1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+    };
+
+    std::unordered_map<uid_t, UidProcStats> expected = {
+            {0,
+             UidProcStats{.totalMajorFaults = 200,
+                          .totalTasksCount = 1,
+                          .ioBlockedTasksCount = 0,
+                          .processStatsByPid = {
+                                  {1, {"random process name with space", 0, 200, 1, 0}}}}}};
+
+    TemporaryDir procDir;
+    ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+                                        perThreadStat));
+
+    UidProcStatsCollector collector(procDir.path);
+
+    ASSERT_TRUE(collector.enabled())
+            << "Files under the path `" << procDir.path << "` are inaccessible";
+    ASSERT_RESULT_OK(collector.collect());
+
+    auto actual = collector.deltaStats();
+
+    EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+            << "Proc pid contents doesn't match.\nExpected:\n"
+            << toString(expected) << "\nActual:\n"
+            << toString(actual);
+}
+
+TEST(UidProcStatsCollectorTest, TestUidProcStatsCollectorContentsFromDevice) {
+    UidProcStatsCollector collector;
+    ASSERT_TRUE(collector.enabled()) << "/proc/[pid]/.* files are inaccessible";
+    ASSERT_RESULT_OK(collector.collect());
+
+    const auto& processStats = collector.deltaStats();
+
+    // The below check should pass because there should be at least one process.
+    EXPECT_GT(processStats.size(), 0);
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h b/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h
new file mode 100644
index 0000000..6e5a409
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_
+#define CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_
+
+#include "UidProcStatsCollector.h"
+
+#include <gmock/gmock.h>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+MATCHER_P(ProcessStatsEq, expected, "") {
+    const auto& actual = arg;
+    return ::testing::Value(actual.comm, ::testing::Eq(expected.comm)) &&
+            ::testing::Value(actual.startTime, ::testing::Eq(expected.startTime)) &&
+            ::testing::Value(actual.totalMajorFaults, ::testing::Eq(expected.totalMajorFaults)) &&
+            ::testing::Value(actual.totalTasksCount, ::testing::Eq(expected.totalTasksCount)) &&
+            ::testing::Value(actual.ioBlockedTasksCount,
+                             ::testing::Eq(expected.ioBlockedTasksCount));
+}
+
+MATCHER(ProcessStatsByPidEq, "") {
+    const auto& actual = std::get<0>(arg);
+    const auto& expected = std::get<1>(arg);
+    return actual.first == expected.first &&
+            ::testing::Value(actual.second, ProcessStatsEq(expected.second));
+}
+
+MATCHER_P(UidProcStatsEq, expected, "") {
+    const auto& actual = arg;
+    return ::testing::Value(actual.totalMajorFaults, ::testing::Eq(expected.totalMajorFaults)) &&
+            ::testing::Value(actual.totalTasksCount, ::testing::Eq(expected.totalTasksCount)) &&
+            ::testing::Value(actual.ioBlockedTasksCount,
+                             ::testing::Eq(expected.ioBlockedTasksCount)) &&
+            ::testing::Value(actual.processStatsByPid,
+                             ::testing::UnorderedPointwise(ProcessStatsByPidEq(),
+                                                           expected.processStatsByPid));
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
+
+#endif  // CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_
diff --git a/cpp/watchdog/server/tests/UidStatsCollectorTest.cpp b/cpp/watchdog/server/tests/UidStatsCollectorTest.cpp
new file mode 100644
index 0000000..7d3bcb1
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidStatsCollectorTest.cpp
@@ -0,0 +1,440 @@
+/*
+ * Copyright 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.
+ */
+
+#include "MockPackageInfoResolver.h"
+#include "MockUidIoStatsCollector.h"
+#include "MockUidProcStatsCollector.h"
+#include "PackageInfoTestUtils.h"
+#include "UidIoStatsCollector.h"
+#include "UidProcStatsCollector.h"
+#include "UidProcStatsCollectorTestUtils.h"
+#include "UidStatsCollector.h"
+
+#include <android-base/stringprintf.h>
+#include <gmock/gmock.h>
+#include <utils/RefBase.h>
+
+#include <string>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::automotive::watchdog::internal::PackageInfo;
+using ::android::automotive::watchdog::internal::UidType;
+using ::android::base::Error;
+using ::android::base::Result;
+using ::android::base::StringAppendF;
+using ::android::base::StringPrintf;
+using ::testing::AllOf;
+using ::testing::Eq;
+using ::testing::ExplainMatchResult;
+using ::testing::Field;
+using ::testing::IsEmpty;
+using ::testing::Matcher;
+using ::testing::Return;
+using ::testing::UnorderedElementsAre;
+using ::testing::UnorderedElementsAreArray;
+
+namespace {
+
+std::string toString(const UidStats& uidStats) {
+    return StringPrintf("UidStats{%s, %s, %s}", uidStats.packageInfo.toString().c_str(),
+                        uidStats.ioStats.toString().c_str(), uidStats.procStats.toString().c_str());
+}
+
+std::string toString(const std::vector<UidStats>& uidStats) {
+    std::string buffer;
+    StringAppendF(&buffer, "{");
+    for (const auto& stats : uidStats) {
+        StringAppendF(&buffer, "%s\n", toString(stats).c_str());
+    }
+    StringAppendF(&buffer, "}");
+    return buffer;
+}
+
+MATCHER_P(UidStatsEq, expected, "") {
+    return ExplainMatchResult(AllOf(Field("packageInfo", &UidStats::packageInfo,
+                                          PackageInfoEq(expected.packageInfo)),
+                                    Field("ioStats", &UidStats::ioStats, Eq(expected.ioStats)),
+                                    Field("procStats", &UidStats::procStats,
+                                          UidProcStatsEq(expected.procStats))),
+                              arg, result_listener);
+}
+
+std::vector<Matcher<const UidStats&>> UidStatsMatchers(const std::vector<UidStats>& uidStats) {
+    std::vector<Matcher<const UidStats&>> matchers;
+    for (const auto& stats : uidStats) {
+        matchers.push_back(UidStatsEq(stats));
+    }
+    return matchers;
+}
+
+std::unordered_map<uid_t, PackageInfo> samplePackageInfoByUid() {
+    return {{1001234, constructPackageInfo("system.daemon", 1001234, UidType::NATIVE)},
+            {1005678, constructPackageInfo("kitchensink.app", 1005678, UidType::APPLICATION)}};
+}
+
+std::unordered_map<uid_t, UidIoStats> sampleUidIoStatsByUid() {
+    return {{1001234,
+             UidIoStats{/*fgRdBytes=*/3'000, /*bgRdBytes=*/0,
+                        /*fgWrBytes=*/500,
+                        /*bgWrBytes=*/0, /*fgFsync=*/20,
+                        /*bgFsync=*/0}},
+            {1005678,
+             UidIoStats{/*fgRdBytes=*/30, /*bgRdBytes=*/100,
+                        /*fgWrBytes=*/50, /*bgWrBytes=*/200,
+                        /*fgFsync=*/45, /*bgFsync=*/60}}};
+}
+
+std::unordered_map<uid_t, UidProcStats> sampleUidProcStatsByUid() {
+    return {{1001234,
+             UidProcStats{.totalMajorFaults = 220,
+                          .totalTasksCount = 2,
+                          .ioBlockedTasksCount = 1,
+                          .processStatsByPid = {{1, {"init", 0, 220, 2, 1}}}}},
+            {1005678,
+             UidProcStats{.totalMajorFaults = 600,
+                          .totalTasksCount = 2,
+                          .ioBlockedTasksCount = 2,
+                          .processStatsByPid = {{1000, {"system_server", 13'400, 600, 2, 2}}}}}};
+}
+
+std::vector<UidStats> sampleUidStats() {
+    return {{.packageInfo = constructPackageInfo("system.daemon", 1001234, UidType::NATIVE),
+             .ioStats = UidIoStats{/*fgRdBytes=*/3'000, /*bgRdBytes=*/0, /*fgWrBytes=*/500,
+                                   /*bgWrBytes=*/0, /*fgFsync=*/20, /*bgFsync=*/0},
+             .procStats = UidProcStats{.totalMajorFaults = 220,
+                                       .totalTasksCount = 2,
+                                       .ioBlockedTasksCount = 1,
+                                       .processStatsByPid = {{1, {"init", 0, 220, 2, 1}}}}},
+            {.packageInfo = constructPackageInfo("kitchensink.app", 1005678, UidType::APPLICATION),
+             .ioStats = UidIoStats{/*fgRdBytes=*/30, /*bgRdBytes=*/100, /*fgWrBytes=*/50,
+                                   /*bgWrBytes=*/200,
+                                   /*fgFsync=*/45, /*bgFsync=*/60},
+             .procStats = UidProcStats{.totalMajorFaults = 600,
+                                       .totalTasksCount = 2,
+                                       .ioBlockedTasksCount = 2,
+                                       .processStatsByPid = {
+                                               {1000, {"system_server", 13'400, 600, 2, 2}}}}}};
+}
+
+}  // namespace
+
+namespace internal {
+
+class UidStatsCollectorPeer : public RefBase {
+public:
+    explicit UidStatsCollectorPeer(sp<UidStatsCollector> collector) : mCollector(collector) {}
+
+    void setPackageInfoResolver(sp<IPackageInfoResolver> packageInfoResolver) {
+        mCollector->mPackageInfoResolver = packageInfoResolver;
+    }
+
+    void setUidIoStatsCollector(sp<UidIoStatsCollectorInterface> uidIoStatsCollector) {
+        mCollector->mUidIoStatsCollector = uidIoStatsCollector;
+    }
+
+    void setUidProcStatsCollector(sp<UidProcStatsCollectorInterface> uidProcStatsCollector) {
+        mCollector->mUidProcStatsCollector = uidProcStatsCollector;
+    }
+
+private:
+    sp<UidStatsCollector> mCollector;
+};
+
+}  // namespace internal
+
+class UidStatsCollectorTest : public ::testing::Test {
+protected:
+    virtual void SetUp() {
+        mUidStatsCollector = sp<UidStatsCollector>::make();
+        mUidStatsCollectorPeer = sp<internal::UidStatsCollectorPeer>::make(mUidStatsCollector);
+        mMockPackageInfoResolver = sp<MockPackageInfoResolver>::make();
+        mMockUidIoStatsCollector = sp<MockUidIoStatsCollector>::make();
+        mMockUidProcStatsCollector = sp<MockUidProcStatsCollector>::make();
+        mUidStatsCollectorPeer->setPackageInfoResolver(mMockPackageInfoResolver);
+        mUidStatsCollectorPeer->setUidIoStatsCollector(mMockUidIoStatsCollector);
+        mUidStatsCollectorPeer->setUidProcStatsCollector(mMockUidProcStatsCollector);
+    }
+
+    virtual void TearDown() {
+        mUidStatsCollector.clear();
+        mUidStatsCollectorPeer.clear();
+        mMockPackageInfoResolver.clear();
+        mMockUidIoStatsCollector.clear();
+        mMockUidProcStatsCollector.clear();
+    }
+
+    sp<UidStatsCollector> mUidStatsCollector;
+    sp<internal::UidStatsCollectorPeer> mUidStatsCollectorPeer;
+    sp<MockPackageInfoResolver> mMockPackageInfoResolver;
+    sp<MockUidIoStatsCollector> mMockUidIoStatsCollector;
+    sp<MockUidProcStatsCollector> mMockUidProcStatsCollector;
+};
+
+TEST_F(UidStatsCollectorTest, TestCollect) {
+    EXPECT_CALL(*mMockUidIoStatsCollector, collect()).WillOnce(Return(Result<void>()));
+    EXPECT_CALL(*mMockUidProcStatsCollector, collect()).WillOnce(Return(Result<void>()));
+
+    EXPECT_CALL(*mMockUidIoStatsCollector, latestStats())
+            .WillOnce(Return(std::unordered_map<uid_t, UidIoStats>()));
+    EXPECT_CALL(*mMockUidProcStatsCollector, latestStats())
+            .WillOnce(Return(std::unordered_map<uid_t, UidProcStats>()));
+
+    EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats())
+            .WillOnce(Return(std::unordered_map<uid_t, UidIoStats>()));
+    EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats())
+            .WillOnce(Return(std::unordered_map<uid_t, UidProcStats>()));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+}
+
+TEST_F(UidStatsCollectorTest, TestFailsCollectOnUidIoStatsCollectorError) {
+    Result<void> errorResult = Error() << "Failed to collect per-UID I/O stats";
+    EXPECT_CALL(*mMockUidIoStatsCollector, collect()).WillOnce(Return(errorResult));
+
+    ASSERT_FALSE(mUidStatsCollector->collect().ok())
+            << "Must fail to collect when per-UID I/O stats collector fails";
+}
+
+TEST_F(UidStatsCollectorTest, TestFailsCollectOnUidProcStatsCollectorError) {
+    Result<void> errorResult = Error() << "Failed to collect per-UID proc stats";
+    EXPECT_CALL(*mMockUidProcStatsCollector, collect()).WillOnce(Return(errorResult));
+
+    ASSERT_FALSE(mUidStatsCollector->collect().ok())
+            << "Must fail to collect when per-UID proc stats collector fails";
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectLatestStats) {
+    const std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+    const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+    const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+    EXPECT_CALL(*mMockPackageInfoResolver,
+                getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+            .WillOnce(Return(packageInfoByUid));
+    EXPECT_CALL(*mMockUidIoStatsCollector, latestStats()).WillOnce(Return(uidIoStatsByUid));
+    EXPECT_CALL(*mMockUidProcStatsCollector, latestStats()).WillOnce(Return(uidProcStatsByUid));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+    const std::vector<UidStats> expected = sampleUidStats();
+
+    auto actual = mUidStatsCollector->latestStats();
+
+    EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+            << "Latest UID stats doesn't match.\nExpected: " << toString(expected)
+            << "\nActual: " << toString(actual);
+
+    actual = mUidStatsCollector->deltaStats();
+
+    EXPECT_THAT(actual, IsEmpty()) << "Delta UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectDeltaStats) {
+    const std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+    const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+    const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+    EXPECT_CALL(*mMockPackageInfoResolver,
+                getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+            .WillOnce(Return(packageInfoByUid));
+    EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+    EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+    const std::vector<UidStats> expected = sampleUidStats();
+
+    auto actual = mUidStatsCollector->deltaStats();
+
+    EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+            << "Delta UID stats doesn't match.\nExpected: " << toString(expected)
+            << "\nActual: " << toString(actual);
+
+    actual = mUidStatsCollector->latestStats();
+
+    EXPECT_THAT(actual, IsEmpty()) << "Latest UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectDeltaStatsWithMissingUidIoStats) {
+    const std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+    std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+    uidIoStatsByUid.erase(1001234);
+    const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+    EXPECT_CALL(*mMockPackageInfoResolver,
+                getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+            .WillOnce(Return(packageInfoByUid));
+    EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+    EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+    std::vector<UidStats> expected = sampleUidStats();
+    expected[0].ioStats = {};
+
+    auto actual = mUidStatsCollector->deltaStats();
+
+    EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+            << "Delta UID stats doesn't match.\nExpected: " << toString(expected)
+            << "\nActual: " << toString(actual);
+
+    actual = mUidStatsCollector->latestStats();
+
+    EXPECT_THAT(actual, IsEmpty()) << "Latest UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectDeltaStatsWithMissingUidProcStats) {
+    const std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+    const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+    std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+    uidProcStatsByUid.erase(1001234);
+
+    EXPECT_CALL(*mMockPackageInfoResolver,
+                getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+            .WillOnce(Return(packageInfoByUid));
+    EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+    EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+    std::vector<UidStats> expected = sampleUidStats();
+    expected[0].procStats = {};
+
+    auto actual = mUidStatsCollector->deltaStats();
+
+    EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+            << "Delta UID stats doesn't match.\nExpected: " << toString(expected)
+            << "\nActual: " << toString(actual);
+
+    actual = mUidStatsCollector->latestStats();
+
+    EXPECT_THAT(actual, IsEmpty()) << "Latest UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectDeltaStatsWithMissingPackageInfo) {
+    std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+    packageInfoByUid.erase(1001234);
+    const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+    const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+    EXPECT_CALL(*mMockPackageInfoResolver,
+                getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+            .WillOnce(Return(packageInfoByUid));
+    EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+    EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+    std::vector<UidStats> expected = sampleUidStats();
+    expected[0].packageInfo = constructPackageInfo("", 1001234);
+
+    auto actual = mUidStatsCollector->deltaStats();
+
+    EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+            << "Delta UID stats doesn't match.\nExpected: " << toString(expected)
+            << "\nActual: " << toString(actual);
+
+    actual = mUidStatsCollector->latestStats();
+
+    EXPECT_THAT(actual, IsEmpty()) << "Latest UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestUidStatsHasPackageInfo) {
+    std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+    packageInfoByUid.erase(1001234);
+    const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+    const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+    EXPECT_CALL(*mMockPackageInfoResolver,
+                getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+            .WillOnce(Return(packageInfoByUid));
+    EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+    EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+    const auto actual = mUidStatsCollector->deltaStats();
+
+    EXPECT_EQ(actual.size(), 2);
+    for (const auto stats : actual) {
+        if (stats.packageInfo.packageIdentifier.uid == 1001234) {
+            EXPECT_FALSE(stats.hasPackageInfo())
+                    << "Stats without package info should return false";
+        } else if (stats.packageInfo.packageIdentifier.uid == 1005678) {
+            EXPECT_TRUE(stats.hasPackageInfo()) << "Stats without package info should return true";
+        } else {
+            FAIL() << "Unexpected uid " << stats.packageInfo.packageIdentifier.uid;
+        }
+    }
+}
+
+TEST_F(UidStatsCollectorTest, TestUidStatsGenericPackageName) {
+    std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+    packageInfoByUid.erase(1001234);
+    const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+    const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+    EXPECT_CALL(*mMockPackageInfoResolver,
+                getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+            .WillOnce(Return(packageInfoByUid));
+    EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+    EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+    const auto actual = mUidStatsCollector->deltaStats();
+
+    EXPECT_EQ(actual.size(), 2);
+    for (const auto stats : actual) {
+        if (stats.packageInfo.packageIdentifier.uid == 1001234) {
+            EXPECT_EQ(stats.genericPackageName(), "1001234")
+                    << "Stats without package info should return UID as package name";
+        } else if (stats.packageInfo.packageIdentifier.uid == 1005678) {
+            EXPECT_EQ(stats.genericPackageName(), "kitchensink.app")
+                    << "Stats with package info should return corresponding package name";
+        } else {
+            FAIL() << "Unexpected uid " << stats.packageInfo.packageIdentifier.uid;
+        }
+    }
+}
+
+TEST_F(UidStatsCollectorTest, TestUidStatsUid) {
+    std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+    packageInfoByUid.erase(1001234);
+    const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+    const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+    EXPECT_CALL(*mMockPackageInfoResolver,
+                getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+            .WillOnce(Return(packageInfoByUid));
+    EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+    EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+    ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+    const auto actual = mUidStatsCollector->deltaStats();
+
+    for (const auto stats : actual) {
+        EXPECT_EQ(stats.uid(), stats.packageInfo.packageIdentifier.uid);
+    }
+}
+
+}  // namespace watchdog
+}  // namespace automotive
+}  // namespace android
diff --git a/cpp/watchdog/server/tests/WatchdogInternalHandlerTest.cpp b/cpp/watchdog/server/tests/WatchdogInternalHandlerTest.cpp
index efa9eef..4646a11 100644
--- a/cpp/watchdog/server/tests/WatchdogInternalHandlerTest.cpp
+++ b/cpp/watchdog/server/tests/WatchdogInternalHandlerTest.cpp
@@ -470,6 +470,19 @@
     ASSERT_FALSE(status.isOk()) << status;
 }
 
+TEST_F(WatchdogInternalHandlerTest, TestControlProcessHealthCheck) {
+    setSystemCallingUid();
+    EXPECT_CALL(*mMockWatchdogProcessService, setEnabled(/*isEnabled=*/true)).Times(1);
+    Status status = mWatchdogInternalHandler->controlProcessHealthCheck(false);
+    ASSERT_TRUE(status.isOk()) << status;
+}
+
+TEST_F(WatchdogInternalHandlerTest, TestErrorOnControlProcessHealthCheckWithNonSystemCallingUid) {
+    EXPECT_CALL(*mMockWatchdogProcessService, setEnabled(_)).Times(0);
+    Status status = mWatchdogInternalHandler->controlProcessHealthCheck(false);
+    ASSERT_FALSE(status.isOk()) << status;
+}
+
 }  // namespace watchdog
 }  // namespace automotive
 }  // namespace android
diff --git a/cpp/watchdog/server/tests/WatchdogPerfServiceTest.cpp b/cpp/watchdog/server/tests/WatchdogPerfServiceTest.cpp
index 2bc9803..864d6d1 100644
--- a/cpp/watchdog/server/tests/WatchdogPerfServiceTest.cpp
+++ b/cpp/watchdog/server/tests/WatchdogPerfServiceTest.cpp
@@ -17,13 +17,10 @@
 #include "LooperStub.h"
 #include "MockDataProcessor.h"
 #include "MockProcDiskStats.h"
-#include "MockProcPidStat.h"
 #include "MockProcStat.h"
-#include "MockUidIoStats.h"
-#include "ProcPidDir.h"
-#include "ProcPidStat.h"
+#include "MockUidStatsCollector.h"
 #include "ProcStat.h"
-#include "UidIoStats.h"
+#include "UidStatsCollector.h"
 #include "WatchdogPerfService.h"
 
 #include <WatchdogProperties.sysprop.h>
@@ -42,12 +39,10 @@
 using ::android::sp;
 using ::android::String16;
 using ::android::wp;
-using ::android::automotive::watchdog::internal::PowerCycle;
 using ::android::automotive::watchdog::testing::LooperStub;
 using ::android::base::Error;
 using ::android::base::Result;
 using ::testing::_;
-using ::testing::DefaultValue;
 using ::testing::InSequence;
 using ::testing::Mock;
 using ::testing::NiceMock;
@@ -71,19 +66,17 @@
 
     void injectFakes() {
         looperStub = sp<LooperStub>::make();
-        mockUidIoStats = sp<NiceMock<MockUidIoStats>>::make();
+        mockUidStatsCollector = sp<MockUidStatsCollector>::make();
         mockProcDiskStats = sp<NiceMock<MockProcDiskStats>>::make();
         mockProcStat = sp<NiceMock<MockProcStat>>::make();
-        mockProcPidStat = sp<NiceMock<MockProcPidStat>>::make();
         mockDataProcessor = sp<StrictMock<MockDataProcessor>>::make();
 
         {
             Mutex::Autolock lock(service->mMutex);
             service->mHandlerLooper = looperStub;
-            service->mUidIoStats = mockUidIoStats;
+            service->mUidStatsCollector = mockUidStatsCollector;
             service->mProcDiskStats = mockProcDiskStats;
             service->mProcStat = mockProcStat;
-            service->mProcPidStat = mockProcPidStat;
         }
         EXPECT_CALL(*mockDataProcessor, init()).Times(1);
         ASSERT_RESULT_OK(service->registerDataProcessor(mockDataProcessor));
@@ -114,19 +107,17 @@
     }
 
     void verifyAndClearExpectations() {
-        Mock::VerifyAndClearExpectations(mockUidIoStats.get());
+        Mock::VerifyAndClearExpectations(mockUidStatsCollector.get());
         Mock::VerifyAndClearExpectations(mockProcStat.get());
-        Mock::VerifyAndClearExpectations(mockProcPidStat.get());
         Mock::VerifyAndClearExpectations(mockDataProcessor.get());
     }
 
     sp<WatchdogPerfService> service;
     // Below fields are populated only on injectFakes.
     sp<LooperStub> looperStub;
-    sp<MockUidIoStats> mockUidIoStats;
+    sp<MockUidStatsCollector> mockUidStatsCollector;
     sp<MockProcDiskStats> mockProcDiskStats;
     sp<MockProcStat> mockProcStat;
-    sp<MockProcPidStat> mockProcPidStat;
     sp<MockDataProcessor> mockDataProcessor;
 };
 
@@ -139,13 +130,13 @@
 
     ASSERT_RESULT_OK(servicePeer->start());
 
-    EXPECT_CALL(*servicePeer->mockUidIoStats, collect()).Times(2);
+    EXPECT_CALL(*servicePeer->mockUidStatsCollector, collect()).Times(2);
     EXPECT_CALL(*servicePeer->mockProcStat, collect()).Times(2);
-    EXPECT_CALL(*servicePeer->mockProcPidStat, collect()).Times(2);
     EXPECT_CALL(*servicePeer->mockDataProcessor,
-                onBoottimeCollection(_, wp<UidIoStats>(servicePeer->mockUidIoStats),
-                                     wp<ProcStat>(servicePeer->mockProcStat),
-                                     wp<ProcPidStat>(servicePeer->mockProcPidStat)))
+                onBoottimeCollection(_,
+                                     wp<UidStatsCollectorInterface>(
+                                             servicePeer->mockUidStatsCollector),
+                                     wp<ProcStat>(servicePeer->mockProcStat)))
             .Times(2);
 
     // Make sure the collection event changes from EventType::INIT to
@@ -206,17 +197,15 @@
 
     ASSERT_RESULT_OK(servicePeer.start());
 
-    wp<UidIoStats> uidIoStats(servicePeer.mockUidIoStats);
+    wp<UidStatsCollectorInterface> uidStatsCollector(servicePeer.mockUidStatsCollector);
     wp<IProcDiskStatsInterface> procDiskStats(servicePeer.mockProcDiskStats);
     wp<ProcStat> procStat(servicePeer.mockProcStat);
-    wp<ProcPidStat> procPidStat(servicePeer.mockProcPidStat);
 
     // #1 Boot-time collection
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-    EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onBoottimeCollection(_, uidIoStats, procStat, procPidStat))
+                onBoottimeCollection(_, uidStatsCollector, procStat))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -228,11 +217,10 @@
     servicePeer.verifyAndClearExpectations();
 
     // #2 Boot-time collection
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-    EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onBoottimeCollection(_, uidIoStats, procStat, procPidStat))
+                onBoottimeCollection(_, uidStatsCollector, procStat))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -245,11 +233,10 @@
     servicePeer.verifyAndClearExpectations();
 
     // #3 Last boot-time collection
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-    EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onBoottimeCollection(_, uidIoStats, procStat, procPidStat))
+                onBoottimeCollection(_, uidStatsCollector, procStat))
             .Times(1);
 
     ASSERT_RESULT_OK(service->onBootFinished());
@@ -286,12 +273,10 @@
     servicePeer.verifyAndClearExpectations();
 
     // #6 Periodic collection
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-    EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onPeriodicCollection(_, SystemState::NORMAL_MODE, uidIoStats, procStat,
-                                     procPidStat))
+                onPeriodicCollection(_, SystemState::NORMAL_MODE, uidStatsCollector, procStat))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -312,12 +297,10 @@
 
     ASSERT_RESULT_OK(service->onCustomCollection(-1, args));
 
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-    EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onCustomCollection(_, SystemState::NORMAL_MODE, _, uidIoStats, procStat,
-                                   procPidStat))
+                onCustomCollection(_, SystemState::NORMAL_MODE, _, uidStatsCollector, procStat))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -329,12 +312,10 @@
     servicePeer.verifyAndClearExpectations();
 
     // #8 Custom collection
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-    EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onCustomCollection(_, SystemState::NORMAL_MODE, _, uidIoStats, procStat,
-                                   procPidStat))
+                onCustomCollection(_, SystemState::NORMAL_MODE, _, uidStatsCollector, procStat))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -362,12 +343,10 @@
             << "Invalid collection event";
 
     // #10 Switch to periodic collection
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-    EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onPeriodicCollection(_, SystemState::NORMAL_MODE, uidIoStats, procStat,
-                                     procPidStat))
+                onPeriodicCollection(_, SystemState::NORMAL_MODE, uidStatsCollector, procStat))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -398,9 +377,8 @@
 
     ASSERT_RESULT_OK(servicePeer.start());
 
-    ON_CALL(*servicePeer.mockUidIoStats, enabled()).WillByDefault(Return(false));
+    ON_CALL(*servicePeer.mockUidStatsCollector, enabled()).WillByDefault(Return(false));
     ON_CALL(*servicePeer.mockProcStat, enabled()).WillByDefault(Return(false));
-    ON_CALL(*servicePeer.mockProcPidStat, enabled()).WillByDefault(Return(false));
 
     // Collection should terminate and call data processor's terminate method on error.
     EXPECT_CALL(*servicePeer.mockDataProcessor, terminate()).Times(1);
@@ -422,7 +400,7 @@
 
     // Inject data collector error.
     Result<void> errorRes = Error() << "Failed to collect data";
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).WillOnce(Return(errorRes));
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).WillOnce(Return(errorRes));
 
     // Collection should terminate and call data processor's terminate method on error.
     EXPECT_CALL(*servicePeer.mockDataProcessor, terminate()).Times(1);
@@ -447,9 +425,10 @@
     // Inject data processor error.
     Result<void> errorRes = Error() << "Failed to process data";
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onBoottimeCollection(_, wp<UidIoStats>(servicePeer.mockUidIoStats),
-                                     wp<ProcStat>(servicePeer.mockProcStat),
-                                     wp<ProcPidStat>(servicePeer.mockProcPidStat)))
+                onBoottimeCollection(_,
+                                     wp<UidStatsCollectorInterface>(
+                                             servicePeer.mockUidStatsCollector),
+                                     wp<ProcStat>(servicePeer.mockProcStat)))
             .WillOnce(Return(errorRes));
 
     // Collection should terminate and call data processor's terminate method on error.
@@ -484,16 +463,15 @@
     int maxIterations = static_cast<int>(kTestCustomCollectionDuration.count() /
                                          kTestCustomCollectionInterval.count());
     for (int i = 0; i <= maxIterations; ++i) {
-        EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+        EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
         EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-        EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
         EXPECT_CALL(*servicePeer.mockDataProcessor,
                     onCustomCollection(_, SystemState::NORMAL_MODE,
                                        UnorderedElementsAreArray(
                                                {"android.car.cts", "system_server"}),
-                                       wp<UidIoStats>(servicePeer.mockUidIoStats),
-                                       wp<ProcStat>(servicePeer.mockProcStat),
-                                       wp<ProcPidStat>(servicePeer.mockProcPidStat)))
+                                       wp<UidStatsCollectorInterface>(
+                                               servicePeer.mockUidStatsCollector),
+                                       wp<ProcStat>(servicePeer.mockProcStat)))
                 .Times(1);
 
         ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -529,10 +507,9 @@
 
     ASSERT_NO_FATAL_FAILURE(startPeriodicCollection(&servicePeer));
 
-    wp<UidIoStats> uidIoStats(servicePeer.mockUidIoStats);
+    wp<UidStatsCollectorInterface> uidStatsCollector(servicePeer.mockUidStatsCollector);
     wp<IProcDiskStatsInterface> procDiskStats(servicePeer.mockProcDiskStats);
     wp<ProcStat> procStat(servicePeer.mockProcStat);
-    wp<ProcPidStat> procPidStat(servicePeer.mockProcPidStat);
 
     // Periodic monitor issuing an alert to start new collection.
     EXPECT_CALL(*servicePeer.mockProcDiskStats, collect()).Times(1);
@@ -549,12 +526,10 @@
             << " seconds interval";
     servicePeer.verifyAndClearExpectations();
 
-    EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+    EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
-    EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onPeriodicCollection(_, SystemState::NORMAL_MODE, uidIoStats, procStat,
-                                     procPidStat))
+                onPeriodicCollection(_, SystemState::NORMAL_MODE, uidStatsCollector, procStat))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -575,7 +550,7 @@
     ASSERT_NO_FATAL_FAILURE(skipPeriodicMonitorEvents(&servicePeer));
 
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onPeriodicCollection(_, SystemState::NORMAL_MODE, _, _, _))
+                onPeriodicCollection(_, SystemState::NORMAL_MODE, _, _))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -587,7 +562,7 @@
     service->setSystemState(SystemState::GARAGE_MODE);
 
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onPeriodicCollection(_, SystemState::GARAGE_MODE, _, _, _))
+                onPeriodicCollection(_, SystemState::GARAGE_MODE, _, _))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -599,7 +574,7 @@
     service->setSystemState(SystemState::NORMAL_MODE);
 
     EXPECT_CALL(*servicePeer.mockDataProcessor,
-                onPeriodicCollection(_, SystemState::NORMAL_MODE, _, _, _))
+                onPeriodicCollection(_, SystemState::NORMAL_MODE, _, _))
             .Times(1);
 
     ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
diff --git a/cpp/watchdog/server/tests/WatchdogServiceHelperTest.cpp b/cpp/watchdog/server/tests/WatchdogServiceHelperTest.cpp
index 99b65df..de05300 100644
--- a/cpp/watchdog/server/tests/WatchdogServiceHelperTest.cpp
+++ b/cpp/watchdog/server/tests/WatchdogServiceHelperTest.cpp
@@ -16,6 +16,7 @@
 
 #include "MockCarWatchdogServiceForSystem.h"
 #include "MockWatchdogProcessService.h"
+#include "PackageInfoTestUtils.h"
 #include "WatchdogServiceHelper.h"
 
 #include <binder/IBinder.h>
@@ -69,22 +70,6 @@
 
 }  // namespace internal
 
-namespace {
-
-PackageInfo constructPackageInfo(const char* packageName, int32_t uid, UidType uidType,
-                                 ComponentType componentType,
-                                 ApplicationCategoryType appCategoryType) {
-    PackageInfo packageInfo;
-    packageInfo.packageIdentifier.name = packageName;
-    packageInfo.packageIdentifier.uid = uid;
-    packageInfo.uidType = uidType;
-    packageInfo.componentType = componentType;
-    packageInfo.appCategoryType = appCategoryType;
-    return packageInfo;
-}
-
-}  // namespace
-
 class WatchdogServiceHelperTest : public ::testing::Test {
 protected:
     virtual void SetUp() {
diff --git a/packages/CarDeveloperOptions/Android.bp b/packages/CarDeveloperOptions/Android.bp
index e3b559a..4394ed2 100644
--- a/packages/CarDeveloperOptions/Android.bp
+++ b/packages/CarDeveloperOptions/Android.bp
@@ -38,4 +38,8 @@
     // TODO(b/176240706): "org.apache.http.legacy" is used by Settings-core,
     // get rid of this dependency and remove the "uses_libs" property.
     uses_libs: ["org.apache.http.legacy"],
+    optional_uses_libs: [
+        "androidx.window.extensions",
+        "androidx.window.sidecar",
+    ],
 }
diff --git a/packages/CarDeveloperOptions/AndroidManifest.xml b/packages/CarDeveloperOptions/AndroidManifest.xml
index cd61380..db784d2 100644
--- a/packages/CarDeveloperOptions/AndroidManifest.xml
+++ b/packages/CarDeveloperOptions/AndroidManifest.xml
@@ -25,6 +25,9 @@
                  tools:node="merge"
                  tools:replace="android:label">
 
+        <uses-library android:name="androidx.window.extensions" android:required="false"/>
+        <uses-library android:name="androidx.window.sidecar" android:required="false"/>
+
         <activity
             android:name=".CarDevelopmentSettingsDashboardActivity"
             android:enabled="false"
diff --git a/packages/CarDeveloperOptions/res/values/strings.xml b/packages/CarDeveloperOptions/res/values/strings.xml
new file mode 100644
index 0000000..5d24671
--- /dev/null
+++ b/packages/CarDeveloperOptions/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="car_pref_category_title">Car</string>
+    <string name="car_ui_plugin_enabled_pref_title">Enable Car UI library plugin</string>
+</resources>
\ No newline at end of file
diff --git a/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentCarUiLibController.java b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentCarUiLibController.java
new file mode 100644
index 0000000..619c565
--- /dev/null
+++ b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentCarUiLibController.java
@@ -0,0 +1,65 @@
+/*
+ * 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.developeroptions;
+
+import android.content.Context;
+import android.os.SystemProperties;
+
+import androidx.preference.Preference;
+import androidx.preference.SwitchPreference;
+
+/**
+ * Preference controller for Car UI library plugin enablement.
+ */
+public class CarDevelopmentCarUiLibController extends CarDevelopmentPreferenceController {
+    private static final String CAR_UI_PLUGIN_ENABLED_KEY = "car_ui_plugin_enabled";
+    static final String CAR_UI_PLUGIN_ENABLED_PROPERTY =
+            "persist.sys.automotive.car.ui.plugin.enabled";
+
+
+    public CarDevelopmentCarUiLibController(Context context) {
+        super(context);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return CAR_UI_PLUGIN_ENABLED_KEY;
+    }
+
+    @Override
+    public String getPreferenceTitle() {
+        return mContext.getString(R.string.car_ui_plugin_enabled_pref_title);
+    }
+
+    @Override
+    String getPreferenceSummary() {
+        return null;
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        SystemProperties.set(CAR_UI_PLUGIN_ENABLED_PROPERTY, String.valueOf(newValue));
+        return true;
+    }
+
+    @Override
+    public void updateState(Preference preference) {
+        final boolean pluginEnabled = SystemProperties.getBoolean(
+                CAR_UI_PLUGIN_ENABLED_PROPERTY, false /* default */);
+        ((SwitchPreference) mPreference).setChecked(pluginEnabled);
+    }
+}
diff --git a/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentPreferenceController.java b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentPreferenceController.java
new file mode 100644
index 0000000..68f6528
--- /dev/null
+++ b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentPreferenceController.java
@@ -0,0 +1,41 @@
+/*
+ * 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.developeroptions;
+
+import android.annotation.Nullable;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+/**
+ * Parent class for Car feature preferences.*
+ */
+public abstract class CarDevelopmentPreferenceController extends
+        DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener,
+        PreferenceControllerMixin {
+
+    abstract String getPreferenceTitle();
+    @Nullable
+    abstract String getPreferenceSummary();
+
+    CarDevelopmentPreferenceController(Context context) {
+        super(context);
+    }
+}
diff --git a/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java
index 72e591f..d74b8ea 100644
--- a/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java
+++ b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java
@@ -24,6 +24,11 @@
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 
+import androidx.annotation.XmlRes;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.SwitchPreference;
+
 import com.android.car.ui.toolbar.MenuItem;
 import com.android.car.ui.toolbar.Toolbar;
 import com.android.car.ui.toolbar.ToolbarController;
@@ -40,27 +45,68 @@
  * {@link PREFERENCES_TO_REMOVE} constant.
  */
 public class CarDevelopmentSettingsDashboardFragment extends DevelopmentSettingsDashboardFragment {
+    static final String PREF_KEY_CAR_CATEGORY = "car_development_category";
+    static final String PREF_KEY_DEBUG_MISC_CATEGORY = "debug_misc_category";
 
-    private ToolbarController mToolbar;
+    private final List<CarDevelopmentPreferenceController> mCarFeatureControllers =
+            new ArrayList<>();
 
     @Override
     public void onActivityCreated(Bundle icicle) {
         super.onActivityCreated(icicle);
-        mToolbar = getToolbar();
-        if (mToolbar != null) {
+        ToolbarController toolbar = getToolbar();
+        if (toolbar != null) {
             List<MenuItem> items = getToolbarMenuItems();
-            mToolbar.setTitle(getPreferenceScreen().getTitle());
-            mToolbar.setMenuItems(items);
-            mToolbar.setNavButtonMode(Toolbar.NavButtonMode.BACK);
-            mToolbar.setState(Toolbar.State.SUBPAGE);
+            toolbar.setTitle(getPreferenceScreen().getTitle());
+            toolbar.setMenuItems(items);
+            toolbar.setNavButtonMode(Toolbar.NavButtonMode.BACK);
+            toolbar.setState(Toolbar.State.SUBPAGE);
         }
     }
 
     @Override
+    public void addPreferencesFromResource(@XmlRes int preferencesResId) {
+        super.addPreferencesFromResource(preferencesResId);
+
+        int miscPrefCategory = getPreferenceScreen().findPreference(
+                PREF_KEY_DEBUG_MISC_CATEGORY).getOrder();
+        PreferenceCategory carCategory = new PreferenceCategory(getContext());
+        carCategory.setOrder(miscPrefCategory + 1);
+        carCategory.setKey(PREF_KEY_CAR_CATEGORY);
+        carCategory.setTitle(getContext().getString(R.string.car_pref_category_title));
+        getPreferenceScreen().addPreference(carCategory);
+
+        for (CarDevelopmentPreferenceController controller : mCarFeatureControllers) {
+            addCarPreference(controller);
+        }
+    }
+
+    void addCarPreference(CarDevelopmentPreferenceController controller) {
+        final Preference dynamicPreference;
+        if (controller instanceof CarDevelopmentCarUiLibController) {
+            dynamicPreference = new SwitchPreference(getContext());
+        } else {
+            throw new UnsupportedOperationException(
+                    "Unexpected controller type " + controller.getClass().getSimpleName());
+        }
+
+        dynamicPreference.setKey(controller.getPreferenceKey());
+        dynamicPreference.setTitle(controller.getPreferenceTitle());
+        final String summary = controller.getPreferenceSummary();
+        if (summary != null) {
+            dynamicPreference.setSummary(summary);
+        }
+
+        ((PreferenceCategory) getPreferenceScreen().findPreference(PREF_KEY_CAR_CATEGORY))
+                .addPreference(dynamicPreference);
+    }
+
+    @Override
     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
         List<AbstractPreferenceController> controllers = super.createPreferenceControllers(context);
         removeControllers(controllers);
         addHiddenControllers(context, controllers);
+        addCarControllers(context, controllers);
         return controllers;
     }
 
@@ -102,6 +148,12 @@
         }
     }
 
+    private void addCarControllers(Context context,
+            List<AbstractPreferenceController> controllers) {
+        mCarFeatureControllers.add(new CarDevelopmentCarUiLibController(context));
+        controllers.addAll(mCarFeatureControllers);
+    }
+
     private boolean isDeveloperOptionsModuleEnabled() {
         PackageManager pm = getContext().getPackageManager();
         ComponentName component = getActivity().getComponentName();
diff --git a/packages/ScriptExecutor/Android.bp b/packages/ScriptExecutor/Android.bp
new file mode 100644
index 0000000..033728b
--- /dev/null
+++ b/packages/ScriptExecutor/Android.bp
@@ -0,0 +1,127 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_defaults {
+    name: "scriptexecutor_defaults",
+
+    cflags: [
+        "-Wno-unused-parameter",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+
+    static_libs: [
+        "liblua",
+    ],
+}
+
+cc_library {
+    name: "libscriptexecutor",
+
+    defaults: [
+        "scriptexecutor_defaults",
+    ],
+
+    srcs: [
+        ":iscriptexecutorconstants_aidl",
+        "src/BundleWrapper.cpp",
+        "src/JniUtils.cpp",
+        "src/LuaEngine.cpp",
+        "src/ScriptExecutorListener.cpp",
+    ],
+
+    shared_libs: [
+        "libbinder",
+        "libnativehelper",
+        "libutils",
+    ],
+
+    // Allow dependents to use the header files.
+    export_include_dirs: ["src"],
+}
+
+cc_library {
+    name: "libscriptexecutorjni",
+
+    defaults: [
+        "scriptexecutor_defaults",
+    ],
+
+    srcs: [
+        "src/ScriptExecutorJni.cpp",
+    ],
+
+    shared_libs: [
+        "libnativehelper",
+        "libscriptexecutor",
+    ],
+}
+
+android_app {
+    name: "ScriptExecutor",
+
+    srcs: [
+        ":iscriptexecutor_aidl",
+        ":iscriptexecutorconstants_aidl",
+        "src/**/*.java"
+    ],
+
+    resource_dirs: ["res"],
+
+    // TODO(197006437): Make this build against sdk_version: "module_current" instead.
+    platform_apis: true,
+
+    privileged: false,
+
+    // TODO(b/196053524): Enable optimization.
+    optimize: {
+        enabled: false,
+    },
+
+    aidl: {
+        include_dirs: [
+            // TODO(b/198195711): Remove once we compile against SDK.
+            "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+        ],
+    },
+
+    jni_libs: [
+        "libscriptexecutorjni",
+    ]
+}
+
+java_test_helper_library {
+    name: "scriptexecutor-test-lib",
+
+    srcs: [
+        ":iscriptexecutor_aidl",
+        ":iscriptexecutorconstants_aidl",
+        "src/**/*.java",
+    ],
+
+    aidl: {
+        include_dirs: [
+            // TODO(b/198195711): Remove once we compile against SDK.
+            "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+        ],
+    },
+}
+
diff --git a/packages/ScriptExecutor/AndroidManifest.xml b/packages/ScriptExecutor/AndroidManifest.xml
new file mode 100644
index 0000000..51595fa
--- /dev/null
+++ b/packages/ScriptExecutor/AndroidManifest.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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.car.scriptexecutor">
+
+    <application android:label="@string/app_title"
+         android:allowBackup="false">
+
+        <service android:name=".ScriptExecutor"
+            android:exported="true"
+            android:isolatedProcess="true"/>
+    </application>
+</manifest>
diff --git a/packages/ScriptExecutor/res/values/strings.xml b/packages/ScriptExecutor/res/values/strings.xml
new file mode 100644
index 0000000..39ca8b2
--- /dev/null
+++ b/packages/ScriptExecutor/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?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_title" translatable="false">Script executor service</string>
+</resources>
diff --git a/packages/ScriptExecutor/src/BundleWrapper.cpp b/packages/ScriptExecutor/src/BundleWrapper.cpp
new file mode 100644
index 0000000..938854f
--- /dev/null
+++ b/packages/ScriptExecutor/src/BundleWrapper.cpp
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+#include "BundleWrapper.h"
+
+#include <android-base/logging.h>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+BundleWrapper::BundleWrapper(JNIEnv* env) {
+    mJNIEnv = env;
+    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));
+}
+
+BundleWrapper::~BundleWrapper() {
+    // Delete global JNI references.
+    if (mBundle != NULL) {
+        mJNIEnv->DeleteGlobalRef(mBundle);
+    }
+    if (mBundleClass != NULL) {
+        mJNIEnv->DeleteGlobalRef(mBundleClass);
+    }
+}
+
+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),
+                            static_cast<jboolean>(value));
+}
+
+void BundleWrapper::putInteger(const char* key, int value) {
+    jmethodID putIntMethod = mJNIEnv->GetMethodID(mBundleClass, "putInt", "(Ljava/lang/String;I)V");
+    mJNIEnv->CallVoidMethod(mBundle, putIntMethod, mJNIEnv->NewStringUTF(key),
+                            static_cast<jint>(value));
+}
+
+void BundleWrapper::putDouble(const char* key, double value) {
+    jmethodID putDoubleMethod =
+            mJNIEnv->GetMethodID(mBundleClass, "putDouble", "(Ljava/lang/String;D)V");
+    mJNIEnv->CallVoidMethod(mBundle, putDoubleMethod, mJNIEnv->NewStringUTF(key),
+                            static_cast<jdouble>(value));
+}
+
+void BundleWrapper::putString(const char* key, const char* value) {
+    jmethodID putStringMethod = mJNIEnv->GetMethodID(mBundleClass, "putString",
+                                                     "(Ljava/lang/String;Ljava/lang/String;)V");
+    // TODO(b/201008922): Handle a case when NewStringUTF returns nullptr (fails
+    // to create a string).
+    mJNIEnv->CallVoidMethod(mBundle, putStringMethod, mJNIEnv->NewStringUTF(key),
+                            mJNIEnv->NewStringUTF(value));
+}
+
+void BundleWrapper::putLongArray(const char* key, const std::vector<int64_t>& value) {
+    jmethodID putLongArrayMethod =
+            mJNIEnv->GetMethodID(mBundleClass, "putLongArray", "(Ljava/lang/String;[J)V");
+
+    jlongArray array = mJNIEnv->NewLongArray(value.size());
+    mJNIEnv->SetLongArrayRegion(array, 0, value.size(), &value[0]);
+    mJNIEnv->CallVoidMethod(mBundle, putLongArrayMethod, mJNIEnv->NewStringUTF(key), array);
+}
+
+void BundleWrapper::putStringArray(const char* key, const std::vector<std::string>& value) {
+    jmethodID putStringArrayMethod =
+            mJNIEnv->GetMethodID(mBundleClass, "putStringArray",
+                                 "(Ljava/lang/String;[Ljava/lang/String;)V");
+
+    jobjectArray array =
+            mJNIEnv->NewObjectArray(value.size(), mJNIEnv->FindClass("java/lang/String"), nullptr);
+    // TODO(b/201008922): Handle a case when NewStringUTF returns nullptr (fails
+    // to create a string).
+    for (int i = 0; i < value.size(); i++) {
+        mJNIEnv->SetObjectArrayElement(array, i, mJNIEnv->NewStringUTF(value[i].c_str()));
+    }
+    mJNIEnv->CallVoidMethod(mBundle, putStringArrayMethod, mJNIEnv->NewStringUTF(key), array);
+}
+
+jobject BundleWrapper::getBundle() {
+    return mBundle;
+}
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
diff --git a/packages/ScriptExecutor/src/BundleWrapper.h b/packages/ScriptExecutor/src/BundleWrapper.h
new file mode 100644
index 0000000..e15c56a
--- /dev/null
+++ b/packages/ScriptExecutor/src/BundleWrapper.h
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+#ifndef PACKAGES_SCRIPTEXECUTOR_SRC_BUNDLEWRAPPER_H_
+#define PACKAGES_SCRIPTEXECUTOR_SRC_BUNDLEWRAPPER_H_
+
+#include "jni.h"
+
+#include <string>
+#include <vector>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+// Used to create a java bundle object and populate its fields one at a time.
+class BundleWrapper {
+public:
+    explicit BundleWrapper(JNIEnv* env);
+    // BundleWrapper is not copyable.
+    BundleWrapper(const BundleWrapper&) = delete;
+    BundleWrapper& operator=(const BundleWrapper&) = delete;
+
+    virtual ~BundleWrapper();
+
+    // 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);
+    void putString(const char* key, const char* value);
+    void putLongArray(const char* key, const std::vector<int64_t>& value);
+    void putStringArray(const char* key, const std::vector<std::string>& value);
+
+    jobject getBundle();
+
+private:
+    // 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 PersistableBundle class cached for performance reasons.
+    jclass mBundleClass;
+
+    // Stores a JNIEnv* pointer.
+    JNIEnv* mJNIEnv;  // not owned
+};
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
+
+#endif  // PACKAGES_SCRIPTEXECUTOR_SRC_BUNDLEWRAPPER_H_
diff --git a/packages/ScriptExecutor/src/JniUtils.cpp b/packages/ScriptExecutor/src/JniUtils.cpp
new file mode 100644
index 0000000..90683ce
--- /dev/null
+++ b/packages/ScriptExecutor/src/JniUtils.cpp
@@ -0,0 +1,156 @@
+/*
+ * 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.
+ */
+
+#include "JniUtils.h"
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+void pushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle) {
+    lua_newtable(luaEngine->getLuaState());
+    // null bundle object is allowed. We will treat it as an empty table.
+    if (bundle == nullptr) {
+        return;
+    }
+
+    // TODO(b/188832769): Consider caching some of these JNI references for
+    // performance reasons.
+    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;");
+    jobject keySetIteratorObject = env->CallObjectMethod(keys, iteratorMethod);
+
+    jclass iteratorClass = env->FindClass("java/util/Iterator");
+    jmethodID hasNextMethod = env->GetMethodID(iteratorClass, "hasNext", "()Z");
+    jmethodID nextMethod = env->GetMethodID(iteratorClass, "next", "()Ljava/lang/Object;");
+
+    jclass booleanClass = env->FindClass("java/lang/Boolean");
+    jclass integerClass = env->FindClass("java/lang/Integer");
+    jclass numberClass = env->FindClass("java/lang/Number");
+    jclass stringClass = env->FindClass("java/lang/String");
+    jclass intArrayClass = env->FindClass("[I");
+    jclass longArrayClass = env->FindClass("[J");
+    jclass stringArrayClass = env->FindClass("[Ljava/lang/String;");
+    // TODO(b/188816922): Handle more types such as float and integer arrays,
+    // and perhaps nested Bundles.
+
+    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)) {
+        // Read the value object that corresponds to this key.
+        jstring key = (jstring)env->CallObjectMethod(keySetIteratorObject, nextMethod);
+        jobject value = env->CallObjectMethod(bundle, getMethod, key);
+
+        // Get the value of the type, extract it accordingly from the bundle and
+        // push the extracted value and the key to the Lua table.
+        if (env->IsInstanceOf(value, booleanClass)) {
+            jmethodID boolMethod = env->GetMethodID(booleanClass, "booleanValue", "()Z");
+            bool boolValue = static_cast<bool>(env->CallBooleanMethod(value, boolMethod));
+            lua_pushboolean(luaEngine->getLuaState(), boolValue);
+        } else if (env->IsInstanceOf(value, integerClass)) {
+            jmethodID intMethod = env->GetMethodID(integerClass, "intValue", "()I");
+            lua_pushinteger(luaEngine->getLuaState(), env->CallIntMethod(value, intMethod));
+        } else if (env->IsInstanceOf(value, numberClass)) {
+            // Condense other numeric types using one class. Because lua supports only
+            // integer or double, and we handled integer in previous if clause.
+            jmethodID numberMethod = env->GetMethodID(numberClass, "doubleValue", "()D");
+            /* Pushes a double onto the stack */
+            lua_pushnumber(luaEngine->getLuaState(), env->CallDoubleMethod(value, numberMethod));
+        } else if (env->IsInstanceOf(value, stringClass)) {
+            // Produces a string in Modified UTF-8 encoding. Any null character
+            // inside the original string is converted into two-byte encoding.
+            // This way we can directly use the output of GetStringUTFChars in C API that
+            // expects a null-terminated string.
+            const char* rawStringValue =
+                    env->GetStringUTFChars(static_cast<jstring>(value), nullptr);
+            lua_pushstring(luaEngine->getLuaState(), rawStringValue);
+            env->ReleaseStringUTFChars(static_cast<jstring>(value), rawStringValue);
+        } else if (env->IsInstanceOf(value, intArrayClass)) {
+            jintArray intArray = static_cast<jintArray>(value);
+            const auto kLength = env->GetArrayLength(intArray);
+            // Arrays are represented as a table of sequential elements in Lua.
+            // We are creating a nested table to represent this array. We specify number of elements
+            // in the Java array to preallocate memory accordingly.
+            lua_createtable(luaEngine->getLuaState(), kLength, 0);
+            jint* rawIntArray = env->GetIntArrayElements(intArray, nullptr);
+            // Fills in the table at stack idx -2 with key value pairs, where key is a
+            // Lua index and value is an integer from the byte array at that index
+            for (int i = 0; i < kLength; i++) {
+                // Stack at index -1 is rawIntArray[i] after this push.
+                lua_pushinteger(luaEngine->getLuaState(), rawIntArray[i]);
+                lua_rawseti(luaEngine->getLuaState(), /* idx= */ -2,
+                            i + 1);  // lua index starts from 1
+            }
+            // JNI_ABORT is used because we do not need to copy back elements.
+            env->ReleaseIntArrayElements(intArray, rawIntArray, JNI_ABORT);
+        } else if (env->IsInstanceOf(value, longArrayClass)) {
+            jlongArray longArray = static_cast<jlongArray>(value);
+            const auto kLength = env->GetArrayLength(longArray);
+            // Arrays are represented as a table of sequential elements in Lua.
+            // We are creating a nested table to represent this array. We specify number of elements
+            // in the Java array to preallocate memory accordingly.
+            lua_createtable(luaEngine->getLuaState(), kLength, 0);
+            jlong* rawLongArray = env->GetLongArrayElements(longArray, nullptr);
+            // Fills in the table at stack idx -2 with key value pairs, where key is a
+            // Lua index and value is an integer from the byte array at that index
+            for (int i = 0; i < kLength; i++) {
+                lua_pushinteger(luaEngine->getLuaState(), rawLongArray[i]);
+                lua_rawseti(luaEngine->getLuaState(), /* idx= */ -2,
+                            i + 1);  // lua index starts from 1
+            }
+            // JNI_ABORT is used because we do not need to copy back elements.
+            env->ReleaseLongArrayElements(longArray, rawLongArray, JNI_ABORT);
+        } else if (env->IsInstanceOf(value, stringArrayClass)) {
+            jobjectArray stringArray = static_cast<jobjectArray>(value);
+            const auto kLength = env->GetArrayLength(stringArray);
+            // Arrays are represented as a table of sequential elements in Lua.
+            // We are creating a nested table to represent this array. We specify number of elements
+            // in the Java array to preallocate memory accordingly.
+            lua_createtable(luaEngine->getLuaState(), kLength, 0);
+            // Fills in the table at stack idx -2 with key value pairs, where key is a Lua index and
+            // value is an string value extracted from the object array at that index
+            for (int i = 0; i < kLength; i++) {
+                jstring element = static_cast<jstring>(env->GetObjectArrayElement(stringArray, i));
+                const char* rawStringValue = env->GetStringUTFChars(element, nullptr);
+                lua_pushstring(luaEngine->getLuaState(), rawStringValue);
+                env->ReleaseStringUTFChars(element, rawStringValue);
+                // lua index starts from 1
+                lua_rawseti(luaEngine->getLuaState(), /* idx= */ -2, i + 1);
+            }
+        } else {
+            // Other types are not implemented yet, skipping.
+            continue;
+        }
+
+        const char* rawKey = env->GetStringUTFChars(key, nullptr);
+        // table[rawKey] = value, where value is on top of the stack,
+        // and the table is the next element in the stack.
+        lua_setfield(luaEngine->getLuaState(), /* idx= */ -2, rawKey);
+        env->ReleaseStringUTFChars(key, rawKey);
+    }
+}
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
diff --git a/packages/ScriptExecutor/src/JniUtils.h b/packages/ScriptExecutor/src/JniUtils.h
new file mode 100644
index 0000000..57aeb9f
--- /dev/null
+++ b/packages/ScriptExecutor/src/JniUtils.h
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+#ifndef PACKAGES_SCRIPTEXECUTOR_SRC_JNIUTILS_H_
+#define PACKAGES_SCRIPTEXECUTOR_SRC_JNIUTILS_H_
+
+#include "LuaEngine.h"
+#include "jni.h"
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+// Helper function which takes android.os.Bundle object in "bundle" argument
+// and converts it to Lua table on top of Lua stack. All key-value pairs are
+// converted to the corresponding key-value pairs of the Lua table as long as
+// the Bundle value types are supported. At this point, we support boolean,
+// integer, double and String types in Java.
+void pushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle);
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
+
+#endif  // PACKAGES_SCRIPTEXECUTOR_SRC_JNIUTILS_H_
diff --git a/packages/ScriptExecutor/src/LuaEngine.cpp b/packages/ScriptExecutor/src/LuaEngine.cpp
new file mode 100644
index 0000000..55bb977
--- /dev/null
+++ b/packages/ScriptExecutor/src/LuaEngine.cpp
@@ -0,0 +1,315 @@
+/*
+ * 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.
+ */
+
+#include "LuaEngine.h"
+
+#include "BundleWrapper.h"
+
+#include <android-base/logging.h>
+#include <com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.h>
+
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+extern "C" {
+#include "lauxlib.h"
+#include "lua.h"
+#include "lualib.h"
+}
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+using ::com::android::car::telemetry::scriptexecutorinterface::IScriptExecutorConstants;
+
+namespace {
+
+enum LuaNumReturnedResults {
+    ZERO_RETURNED_RESULTS = 0,
+};
+
+// TODO(199415783): Revisit the topic of limits to potentially move it to standalone file.
+constexpr int MAX_ARRAY_SIZE = 1000;
+
+// 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.
+//
+// Returns false if the conversion encountered unrecoverable error.
+// Otherwise, returns true for success.
+// TODO(b/200849134): Refactor this function.
+bool convertLuaTableToBundle(lua_State* lua, BundleWrapper* bundleWrapper,
+                             ScriptExecutorListener* listener) {
+    // 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.
+        // TODO(199531928): Consider putting limits on key sizes as well.
+        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)) {
+            // TODO(199415783): We need to have a limit on how long these strings could be.
+            bundleWrapper->putString(key, lua_tostring(lua, /* index = */ -1));
+        } else if (lua_istable(lua, /* index =*/-1)) {
+            // Lua uses tables to represent an array.
+
+            // TODO(199438375): Document to users that we expect tables to be either only indexed or
+            // keyed but not both. If the table contains consecutively indexed values starting from
+            // 1, we will treat it as an array. lua_rawlen call returns the size of the indexed
+            // part. We copy this part into an array, but any keyed values in this table are
+            // ignored. There is a test that documents this current behavior. If a user wants a
+            // nested table to be represented by a PersistableBundle object, they must make sure
+            // that the nested table does not contain indexed data, including no key=1.
+            const auto kTableLength = lua_rawlen(lua, -1);
+            if (kTableLength > MAX_ARRAY_SIZE) {
+                std::ostringstream out;
+                out << "Returned table " << key << " exceeds maximum allowed size of "
+                    << MAX_ARRAY_SIZE
+                    << " elements. This key-value cannot be unpacked successfully. This error "
+                       "is unrecoverable.";
+                listener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
+                                  out.str().c_str(), "");
+                return false;
+            }
+            if (kTableLength <= 0) {
+                std::ostringstream out;
+                out << "A value with key=" << key
+                    << " appears to be a nested table that does not represent an array of data. "
+                       "Such nested tables are not supported yet. This script error is "
+                       "unrecoverable.";
+                listener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
+                                  out.str().c_str(), "");
+                return false;
+            }
+
+            std::vector<int64_t> longArray;
+            std::vector<std::string> stringArray;
+            int originalLuaType = LUA_TNIL;
+            for (int i = 0; i < kTableLength; i++) {
+                lua_rawgeti(lua, -1, i + 1);
+                // Lua allows arrays to have values of varying type. We need to force all Lua
+                // arrays to stick to single type within the same array. We use the first value
+                // in the array to determine the type of all values in the array that follow
+                // after. If the second, third, etc element of the array does not match the type
+                // of the first element we stop the extraction and return an error via a
+                // callback.
+                if (i == 0) {
+                    originalLuaType = lua_type(lua, /* index = */ -1);
+                }
+                int currentType = lua_type(lua, /* index= */ -1);
+                if (currentType != originalLuaType) {
+                    std::ostringstream out;
+                    out << "Returned Lua arrays must have elements of the same type. Returned "
+                           "table with key="
+                        << key << " has the first element of type=" << originalLuaType
+                        << ", but the element at index=" << i + 1 << " has type=" << currentType
+                        << ". Integer type codes are defined in lua.h file. This error is "
+                           "unrecoverable.";
+                    listener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
+                                      out.str().c_str(), "");
+                    lua_pop(lua, 1);
+                    return false;
+                }
+                switch (currentType) {
+                    case LUA_TNUMBER:
+                        if (!lua_isinteger(lua, /* index = */ -1)) {
+                            LOG(WARNING) << "Floating array types are not supported yet. Skipping.";
+                        } else {
+                            longArray.push_back(lua_tointeger(lua, /* index = */ -1));
+                        }
+                        break;
+                    case LUA_TSTRING:
+                        // TODO(b/200833728): Investigate optimizations to minimize string
+                        // copying. For example, populate JNI object array one element at a
+                        // time, as we go.
+                        stringArray.push_back(lua_tostring(lua, /* index = */ -1));
+                        break;
+                    default:
+                        LOG(WARNING) << "Lua array with elements of type=" << currentType
+                                     << " are not supported. Skipping.";
+                }
+                lua_pop(lua, 1);
+            }
+            switch (originalLuaType) {
+                case LUA_TNUMBER:
+                    bundleWrapper->putLongArray(key, longArray);
+                    break;
+                case LUA_TSTRING:
+                    bundleWrapper->putStringArray(key, stringArray);
+                    break;
+            }
+        } else {
+            // not supported yet...
+            // TODO(199439259): Instead of logging here, log and send to user instead, and continue
+            // unpacking the rest of the table.
+            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.
+    }
+    return true;
+}
+
+}  // namespace
+
+ScriptExecutorListener* LuaEngine::sListener = nullptr;
+
+LuaEngine::LuaEngine() {
+    // Instantiate Lua environment
+    mLuaState = luaL_newstate();
+    luaL_openlibs(mLuaState);
+}
+
+LuaEngine::~LuaEngine() {
+    lua_close(mLuaState);
+}
+
+lua_State* LuaEngine::getLuaState() {
+    return mLuaState;
+}
+
+void LuaEngine::resetListener(ScriptExecutorListener* listener) {
+    if (sListener != nullptr) {
+        delete sListener;
+    }
+    sListener = listener;
+}
+
+int LuaEngine::loadScript(const char* scriptBody) {
+    // As the first step in Lua script execution we want to load
+    // the body of the script into Lua stack and have it processed by Lua
+    // to catch any errors.
+    // More on luaL_dostring: https://www.lua.org/manual/5.3/manual.html#lual_dostring
+    // If error, pushes the error object into the stack.
+    const auto status = luaL_dostring(mLuaState, scriptBody);
+    if (status) {
+        // Removes error object from the stack.
+        // Lua stack must be properly maintained due to its limited size,
+        // ~20 elements and its critical function because all interaction with
+        // Lua happens via the stack.
+        // Starting read about Lua stack: https://www.lua.org/pil/24.2.html
+        // TODO(b/192284232): add test case to trigger this.
+        lua_pop(mLuaState, 1);
+        return status;
+    }
+
+    // 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;
+}
+
+bool LuaEngine::pushFunction(const char* functionName) {
+    // Interaction between native code and Lua happens via Lua stack.
+    // In such model, a caller first pushes the name of the function
+    // that needs to be called, followed by the function's input
+    // arguments, one input value pushed at a time.
+    // More info: https://www.lua.org/pil/24.2.html
+    lua_getglobal(mLuaState, functionName);
+    const auto status = lua_isfunction(mLuaState, /*idx= */ -1);
+    // TODO(b/192284785): add test case for wrong function name in Lua.
+    if (status == 0) lua_pop(mLuaState, 1);
+    return status;
+}
+
+int LuaEngine::run() {
+    // Performs blocking call of the provided Lua function. Assumes all
+    // input arguments are in the Lua stack as well in proper order.
+    // On how to call Lua functions: https://www.lua.org/pil/25.2.html
+    // Doc on lua_pcall: https://www.lua.org/manual/5.3/manual.html#lua_pcall
+    // TODO(b/189241508): Once we implement publishedData parsing, nargs should
+    // change from 1 to 2.
+    // TODO(b/192284612): add test case for failed call.
+    return lua_pcall(mLuaState, /* nargs= */ 1, /* nresults= */ 0, /*errfunc= */ 0);
+}
+
+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)) {
+        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 PersistableBundle object.
+    BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
+    if (convertLuaTableToBundle(lua, &bundleWrapper, sListener)) {
+        // Forward the populated Bundle object to Java callback.
+        sListener->onSuccess(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::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());
+    if (convertLuaTableToBundle(lua, &bundleWrapper, sListener)) {
+        // 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)) {
+        sListener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
+                           "on_error can push only a single string parameter from Lua", "");
+        return ZERO_RETURNED_RESULTS;
+    }
+    sListener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
+                       lua_tostring(lua, /* index = */ -1), /* stackTrace =*/"");
+    return ZERO_RETURNED_RESULTS;
+}
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
diff --git a/packages/ScriptExecutor/src/LuaEngine.h b/packages/ScriptExecutor/src/LuaEngine.h
new file mode 100644
index 0000000..0774108
--- /dev/null
+++ b/packages/ScriptExecutor/src/LuaEngine.h
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+
+#ifndef PACKAGES_SCRIPTEXECUTOR_SRC_LUAENGINE_H_
+#define PACKAGES_SCRIPTEXECUTOR_SRC_LUAENGINE_H_
+
+#include "ScriptExecutorListener.h"
+
+#include <memory>
+
+extern "C" {
+#include "lua.h"
+}
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+// Encapsulates Lua script execution environment.
+class LuaEngine {
+public:
+    LuaEngine();
+
+    virtual ~LuaEngine();
+
+    // Returns pointer to Lua state object.
+    lua_State* getLuaState();
+
+    // Loads Lua script provided as scriptBody string.
+    // Returns 0 if successful. Otherwise returns non-zero Lua error code.
+    int loadScript(const char* scriptBody);
+
+    // Pushes a Lua function under provided name into the stack.
+    // Returns true if successful.
+    bool pushFunction(const char* functionName);
+
+    // Invokes function with the inputs provided in the stack.
+    // Assumes that the script body has been already loaded and successfully
+    // compiled and run, and all input arguments, and the function have been
+    // pushed to the stack.
+    // Returns 0 if successful. Otherwise returns non-zero Lua error code.
+    int run();
+
+    // Updates stored listener and destroys the previous one.
+    static void resetListener(ScriptExecutorListener* listener);
+
+private:
+    // Invoked by a running Lua script to store intermediate results.
+    // 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 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.
+    // 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 onError(lua_State* lua);
+
+    // Points to the current listener object.
+    // Lua cannot call non-static class methods. We need to access listener object instance in
+    // Lua callbacks. Therefore, callbacks callable by Lua are static class methods and the pointer
+    // to a listener object needs to be static, since static methods cannot access non-static
+    // members.
+    // Only one listener is supported at any given time.
+    // Since listeners are heap-allocated, the destructor does not need to run at shutdown
+    // of the service because the memory allocated to the current listener object will be
+    // reclaimed by the OS.
+    static ScriptExecutorListener* sListener;
+
+    lua_State* mLuaState;  // owned
+};
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
+
+#endif  // PACKAGES_SCRIPTEXECUTOR_SRC_LUAENGINE_H_
diff --git a/packages/ScriptExecutor/src/ScriptExecutorJni.cpp b/packages/ScriptExecutor/src/ScriptExecutorJni.cpp
new file mode 100644
index 0000000..c11a30b
--- /dev/null
+++ b/packages/ScriptExecutor/src/ScriptExecutorJni.cpp
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+#include "JniUtils.h"
+#include "LuaEngine.h"
+#include "ScriptExecutorListener.h"
+#include "jni.h"
+
+#include <android-base/logging.h>
+
+#include <cstdint>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+extern "C" {
+
+JNIEXPORT jlong JNICALL Java_com_android_car_scriptexecutor_ScriptExecutor_nativeInitLuaEngine(
+        JNIEnv* env, jobject object) {
+    // Cast first to intptr_t to ensure int can hold the pointer without loss.
+    return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
+}
+
+JNIEXPORT void JNICALL Java_com_android_car_scriptexecutor_ScriptExecutor_nativeDestroyLuaEngine(
+        JNIEnv* env, jobject object, jlong luaEnginePtr) {
+    delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+}
+
+// Parses the inputs and loads them to Lua one at a time.
+// Loading of data into Lua also triggers checks on Lua side to verify the
+// inputs are valid. For example, pushing "functionName" into Lua stack verifies
+// that the function name actually exists in the previously loaded body of the
+// script.
+//
+// The steps are:
+// Step 1: Parse the inputs for obvious programming errors.
+// Step 2: Parse and load the body of the script.
+// Step 3: Parse and push function name we want to execute in the provided
+// script body to Lua stack. If the function name doesn't exist, we exit.
+// Step 4: Parse publishedData, convert it into Lua table and push it to the
+// stack.
+// Step 5: Parse savedState Bundle object, convert it into Lua table and push it
+// to the stack.
+// Any errors that occur at the stage above result in quick exit or crash.
+//
+// All interaction with Lua happens via Lua stack. Therefore, order of how the
+// inputs are parsed and processed is critical because Lua API methods such as
+// lua_pcall assume specific order between function name and the input arguments
+// on the stack.
+// More information about how to work with Lua stack: https://www.lua.org/pil/24.2.html
+// and how Lua functions are called via Lua API: https://www.lua.org/pil/25.2.html
+//
+// Finally, once parsing and pushing to Lua stack is complete, we go on to the final step,
+// Step 6: Attempt to run the provided function.
+JNIEXPORT void JNICALL Java_com_android_car_scriptexecutor_ScriptExecutor_nativeInvokeScript(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring scriptBody, jstring functionName,
+        jobject publishedData, jobject savedState, jobject listener) {
+    if (!luaEnginePtr) {
+        env->FatalError("luaEnginePtr parameter cannot be nil");
+    }
+    if (scriptBody == nullptr) {
+        env->FatalError("scriptBody parameter cannot be null");
+    }
+    if (functionName == nullptr) {
+        env->FatalError("functionName parameter cannot be null");
+    }
+    if (listener == nullptr) {
+        env->FatalError("listener parameter cannot be null");
+    }
+
+    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+
+    // Load and parse the script
+    const char* scriptStr = env->GetStringUTFChars(scriptBody, nullptr);
+    auto status = engine->loadScript(scriptStr);
+    env->ReleaseStringUTFChars(scriptBody, scriptStr);
+    // status == 0 if the script loads successfully.
+    if (status) {
+        env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
+                      "Failed to load the script.");
+        return;
+    }
+    LuaEngine::resetListener(new ScriptExecutorListener(env, listener));
+
+    // Push the function name we want to invoke to Lua stack
+    const char* functionNameStr = env->GetStringUTFChars(functionName, nullptr);
+    status = engine->pushFunction(functionNameStr);
+    env->ReleaseStringUTFChars(functionName, functionNameStr);
+    // status == 1 if the name is indeed a function.
+    if (!status) {
+        env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
+                      "symbol functionName does not correspond to a function.");
+        return;
+    }
+
+    // TODO(b/189241508): Provide implementation to parse publishedData input,
+    // convert it into Lua table and push into Lua stack.
+    if (publishedData) {
+        LOG(WARNING) << "Parsing of publishedData is not implemented yet.";
+    }
+
+    // Unpack bundle in savedState, convert to Lua table and push it to Lua
+    // stack.
+    pushBundleToLuaTable(env, engine, savedState);
+
+    // Execute the function. This will block until complete or error.
+    if (engine->run()) {
+        env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
+                      "Runtime error occurred while running the function.");
+        return;
+    }
+}
+
+}  // extern "C"
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
diff --git a/packages/ScriptExecutor/src/ScriptExecutorListener.cpp b/packages/ScriptExecutor/src/ScriptExecutorListener.cpp
new file mode 100644
index 0000000..739c71d
--- /dev/null
+++ b/packages/ScriptExecutor/src/ScriptExecutorListener.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+
+#include "ScriptExecutorListener.h"
+
+#include <android-base/logging.h>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+ScriptExecutorListener::~ScriptExecutorListener() {
+    JNIEnv* env = getCurrentJNIEnv();
+    if (mScriptExecutorListener != nullptr) {
+        env->DeleteGlobalRef(mScriptExecutorListener);
+    }
+}
+
+ScriptExecutorListener::ScriptExecutorListener(JNIEnv* env, jobject script_executor_listener) {
+    mScriptExecutorListener = env->NewGlobalRef(script_executor_listener);
+    env->GetJavaVM(&mJavaVM);
+}
+
+void ScriptExecutorListener::onSuccess(jobject bundle) {
+    JNIEnv* env = getCurrentJNIEnv();
+    jclass listenerClass = env->GetObjectClass(mScriptExecutorListener);
+    jmethodID onSuccessMethod =
+            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();
+    jclass listenerClass = env->GetObjectClass(mScriptExecutorListener);
+    jmethodID onErrorMethod =
+            env->GetMethodID(listenerClass, "onError", "(ILjava/lang/String;Ljava/lang/String;)V");
+
+    env->CallVoidMethod(mScriptExecutorListener, onErrorMethod, errorType,
+                        env->NewStringUTF(message), env->NewStringUTF(stackTrace));
+}
+
+JNIEnv* ScriptExecutorListener::getCurrentJNIEnv() {
+    JNIEnv* env;
+    if (mJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        LOG(FATAL) << "Unable to return JNIEnv from JavaVM";
+    }
+    return env;
+}
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
diff --git a/packages/ScriptExecutor/src/ScriptExecutorListener.h b/packages/ScriptExecutor/src/ScriptExecutorListener.h
new file mode 100644
index 0000000..392bc77
--- /dev/null
+++ b/packages/ScriptExecutor/src/ScriptExecutorListener.h
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+#ifndef PACKAGES_SCRIPTEXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
+#define PACKAGES_SCRIPTEXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
+
+#include "jni.h"
+
+#include <string>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+//  Wrapper class for IScriptExecutorListener.aidl.
+class ScriptExecutorListener {
+public:
+    ScriptExecutorListener(JNIEnv* jni, jobject script_executor_listener);
+
+    virtual ~ScriptExecutorListener();
+
+    void onScriptFinished(jobject bundle);
+
+    void onSuccess(jobject bundle);
+
+    void onError(const int errorType, const char* message, const char* stackTrace);
+
+    JNIEnv* getCurrentJNIEnv();
+
+private:
+    // Stores a jni global reference to Java Script Executor listener object.
+    jobject mScriptExecutorListener;
+
+    // Stores JavaVM pointer in order to be able to get JNIEnv pointer.
+    // This is done because JNIEnv cannot be shared between threads.
+    // https://developer.android.com/training/articles/perf-jni.html#javavm-and-jnienv
+    JavaVM* mJavaVM;
+};
+
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
+
+#endif  // PACKAGES_SCRIPTEXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
diff --git a/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java b/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java
new file mode 100644
index 0000000..d2cb1c8
--- /dev/null
+++ b/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java
@@ -0,0 +1,152 @@
+/*
+ * 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.scriptexecutor;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorConstants;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Executes Lua code in an isolated process with provided source code
+ * and input arguments.
+ */
+public final class ScriptExecutor extends Service {
+
+    static {
+        System.loadLibrary("scriptexecutorjni");
+    }
+
+    private static final String TAG = ScriptExecutor.class.getSimpleName();
+
+    // Dedicated "worker" thread to handle all calls related to native code.
+    private HandlerThread mNativeHandlerThread;
+    // Handler associated with the native worker thread.
+    private Handler mNativeHandler;
+
+    private final class IScriptExecutorImpl extends IScriptExecutor.Stub {
+        @Override
+        public void invokeScript(String scriptBody, String functionName,
+                PersistableBundle publishedData, PersistableBundle savedState,
+                IScriptExecutorListener listener) {
+            mNativeHandler.post(() ->
+                    nativeInvokeScript(mLuaEnginePtr, scriptBody, functionName, publishedData,
+                            savedState, listener));
+        }
+
+        @Override
+        public void invokeScriptForLargeInput(String scriptBody, String functionName,
+                ParcelFileDescriptor publishedDataFileDescriptor, PersistableBundle savedState,
+                IScriptExecutorListener listener) {
+            mNativeHandler.post(() -> {
+                PersistableBundle publishedData;
+                try (InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(
+                        publishedDataFileDescriptor)) {
+                    publishedData = PersistableBundle.readFromStream(input);
+                } catch (IOException e) {
+                    try {
+                        listener.onError(IScriptExecutorConstants.ERROR_TYPE_SCRIPT_EXECUTOR_ERROR,
+                                e.getMessage(), "");
+                    } catch (RemoteException remoteException) {
+                        if (Log.isLoggable(TAG, Log.ERROR)) {
+                            // At least log "message" here, in case it was never sent back via
+                            // the callback.
+                            Slog.e(TAG, "failed while calling listener with exception ", e);
+                        }
+                    }
+                    return;
+                }
+
+                // TODO(b/189241508): Start passing publishedData instead of null
+                // once publishedData parser is implemented.
+                nativeInvokeScript(mLuaEnginePtr, scriptBody, functionName, null,
+                        savedState, listener);
+            });
+        }
+    }
+
+    private IScriptExecutorImpl mScriptExecutorBinder;
+
+    // Memory location of Lua Engine object which is allocated in native code.
+    private long mLuaEnginePtr;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        mNativeHandlerThread = new HandlerThread(ScriptExecutor.class.getSimpleName());
+        mNativeHandlerThread.start();
+        mNativeHandler = new Handler(mNativeHandlerThread.getLooper());
+
+        mLuaEnginePtr = nativeInitLuaEngine();
+        mScriptExecutorBinder = new IScriptExecutorImpl();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        nativeDestroyLuaEngine(mLuaEnginePtr);
+        mNativeHandlerThread.quit();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mScriptExecutorBinder;
+    }
+
+    /**
+     * Initializes Lua Engine.
+     *
+     * <p>Returns memory location of Lua Engine.
+     */
+    private native long nativeInitLuaEngine();
+
+    /**
+     * Destroys LuaEngine at the provided memory address.
+     */
+    private native void nativeDestroyLuaEngine(long luaEnginePtr);
+
+    /**
+     * Calls provided Lua function.
+     *
+     * @param luaEnginePtr  memory address of the stored LuaEngine instance.
+     * @param scriptBody    complete body of Lua script that also contains the function to be
+     *                      invoked.
+     * @param functionName  the name of the function to execute.
+     * @param publishedData input data provided by the source which the function handles.
+     * @param savedState    key-value pairs preserved from the previous invocation of the function.
+     * @param listener      callback for the sandboxed environent to report back script execution
+     *                      results
+     *                      and errors.
+     */
+    private native void nativeInvokeScript(long luaEnginePtr, String scriptBody,
+            String functionName, PersistableBundle publishedData, PersistableBundle savedState,
+            IScriptExecutorListener listener);
+}
diff --git a/packages/ScriptExecutor/tests/unit/Android.bp b/packages/ScriptExecutor/tests/unit/Android.bp
new file mode 100644
index 0000000..fbbe750
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/Android.bp
@@ -0,0 +1,63 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "ScriptExecutorUnitTest",
+
+    srcs: ["src/**/*.java"],
+
+    platform_apis: true,
+
+    certificate: "platform",
+
+    instrumentation_for: "ScriptExecutor",
+
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.runner",
+        "junit",
+        "scriptexecutor-test-lib",
+        "truth-prebuilt",
+    ],
+
+    test_suites: ["general-tests"],
+
+    jni_libs: [
+        "libscriptexecutorjni",
+        "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
new file mode 100644
index 0000000..f393408
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/AndroidManifest.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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          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_test"
+                     android:label="Tests for ScriptExecutor"/>
+</manifest>
diff --git a/packages/ScriptExecutor/tests/unit/README.md b/packages/ScriptExecutor/tests/unit/README.md
new file mode 100644
index 0000000..8c5a67a
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/README.md
@@ -0,0 +1,52 @@
+<!--
+  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
+  -->
+
+# How to run unit tests for ScriptExecutor
+
+**1. Navigate to the root of the repo and do full build:**
+
+`m -j`
+
+**2. Flash the device with this build:**
+
+`aae flash`
+
+**3. Run the tests. For example**
+
+`atest ScriptExecutorUnitTest:ScriptExecutorTest`
+
+
+## How to rerun the tests after changes
+Sometimes a test needs to be modified. These are the steps to do incremental update instead of full
+device flash.
+
+**1. Navigate to ScriptExecutor unit test location and build its targets:**
+`cd packages/services/Car/packages/ScriptExecutor/tests/unit`
+
+`mm -j`
+
+**2. Sync the device with all the files that need to be updated:**
+
+`adb root`
+
+`adb remount`
+
+`adb sync && adb shell stop && adb shell start`
+
+**3. At this point we are ready to run the tests again. For example:**
+
+`atest ScriptExecutorUnitTest:ScriptExecutorTest`
+
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
new file mode 100644
index 0000000..3292009
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.scriptexecutor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JniUtilsTest {
+
+    private static final String TAG = JniUtilsTest.class.getSimpleName();
+
+    private static final String BOOLEAN_KEY = "boolean_key";
+    private static final String INT_KEY = "int_key";
+    private static final String STRING_KEY = "string_key";
+    private static final String NUMBER_KEY = "number_key";
+    private static final String INT_ARRAY_KEY = "int_array_key";
+    private static final String LONG_ARRAY_KEY = "long_array_key";
+
+    private static final boolean BOOLEAN_VALUE = true;
+    private static final double NUMBER_VALUE = 0.1;
+    private static final int INT_VALUE = 10;
+    private static final String STRING_VALUE = "test";
+    private static final int[] INT_ARRAY_VALUE = new int[]{1, 2, 3};
+    private static final long[] LONG_ARRAY_VALUE = new long[]{1, 2, 3, 4};
+
+    // Pointer to Lua Engine instantiated in native space.
+    private long mLuaEnginePtr = 0;
+
+    static {
+        System.loadLibrary("scriptexecutorjniutils-test");
+    }
+
+    @Before
+    public void setUp() {
+        mLuaEnginePtr = nativeCreateLuaEngine();
+    }
+
+    @After
+    public void tearDown() {
+        nativeDestroyLuaEngine(mLuaEnginePtr);
+    }
+
+    // Simply invokes PushBundleToLuaTable native method under test.
+    private native void nativePushBundleToLuaTableCaller(
+            long luaEnginePtr, PersistableBundle bundle);
+
+    // Creates an instance of LuaEngine on the heap and returns the pointer.
+    private native long nativeCreateLuaEngine();
+
+    // Destroys instance of LuaEngine on the native side at provided memory address.
+    private native void nativeDestroyLuaEngine(long luaEnginePtr);
+
+    // Returns size of a Lua object located at the specified position on the stack.
+    private native int nativeGetObjectSize(long luaEnginePtr, int index);
+
+    /*
+     * Family of methods to check if the table on top of the stack has
+     * the given value under provided key.
+     */
+    private native boolean nativeHasBooleanValue(long luaEnginePtr, String key, boolean value);
+
+    private native boolean nativeHasStringValue(long luaEnginePtr, String key, String value);
+
+    private native boolean nativeHasIntValue(long luaEnginePtr, String key, int value);
+
+    private native boolean nativeHasDoubleValue(long luaEnginePtr, String key, double value);
+
+    private native boolean nativeHasIntArrayValue(long luaEnginePtr, String key, int[] value);
+
+    private native boolean nativeHasLongArrayValue(long luaEnginePtr, String key, long[] value);
+
+    @Test
+    public void pushBundleToLuaTable_nullBundleMakesEmptyLuaTable() {
+        nativePushBundleToLuaTableCaller(mLuaEnginePtr, null);
+        // Get the size of the object on top of the stack,
+        // which is where our table is supposed to be.
+        assertThat(nativeGetObjectSize(mLuaEnginePtr, 1)).isEqualTo(0);
+    }
+
+    @Test
+    public void pushBundleToLuaTable_valuesOfDifferentTypes() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
+        bundle.putInt(INT_KEY, INT_VALUE);
+        bundle.putDouble(NUMBER_KEY, NUMBER_VALUE);
+        bundle.putString(STRING_KEY, STRING_VALUE);
+
+        // Invokes the corresponding helper method to convert the bundle
+        // to Lua table on Lua stack.
+        nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
+
+        // Check contents of Lua table.
+        assertThat(nativeHasBooleanValue(mLuaEnginePtr, BOOLEAN_KEY, BOOLEAN_VALUE)).isTrue();
+        assertThat(nativeHasIntValue(mLuaEnginePtr, INT_KEY, INT_VALUE)).isTrue();
+        assertThat(nativeHasDoubleValue(mLuaEnginePtr, NUMBER_KEY, NUMBER_VALUE)).isTrue();
+        assertThat(nativeHasStringValue(mLuaEnginePtr, STRING_KEY, STRING_VALUE)).isTrue();
+    }
+
+    @Test
+    public void pushBundleToLuaTable_wrongKey() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
+
+        // Invokes the corresponding helper method to convert the bundle
+        // to Lua table on Lua stack.
+        nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
+
+        // Check contents of Lua table.
+        assertThat(nativeHasBooleanValue(mLuaEnginePtr, "wrong key", BOOLEAN_VALUE)).isFalse();
+    }
+
+    @Test
+    public void pushBundleToLuaTable_arrays() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putIntArray(INT_ARRAY_KEY, INT_ARRAY_VALUE);
+        bundle.putLongArray(LONG_ARRAY_KEY, LONG_ARRAY_VALUE);
+
+        // Invokes the corresponding helper method to convert the bundle
+        // to Lua table on Lua stack.
+        nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
+
+        // Check contents of Lua table.
+        // Java int and long arrays both end up being arrays of Lua's Integer type,
+        // which is interpreted as a 8-byte int type.
+        assertThat(nativeHasIntArrayValue(mLuaEnginePtr, INT_ARRAY_KEY, INT_ARRAY_VALUE)).isTrue();
+        assertThat(
+                nativeHasLongArrayValue(mLuaEnginePtr, LONG_ARRAY_KEY, LONG_ARRAY_VALUE)).isTrue();
+    }
+}
diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTestHelper.cpp b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTestHelper.cpp
new file mode 100644
index 0000000..efc34d3
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTestHelper.cpp
@@ -0,0 +1,195 @@
+/*
+ * 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.
+ */
+
+#include "JniUtils.h"
+#include "LuaEngine.h"
+#include "jni.h"
+
+#include <cstdint>
+#include <cstring>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+namespace {
+
+template <typename T>
+bool hasIntegerArray(JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, T rawInputArray,
+                     const int arrayLength) {
+    const char* rawKey = env->GetStringUTFChars(key, nullptr);
+    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+    // Assumes the table is on top of the stack.
+    auto* luaState = engine->getLuaState();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_istable(luaState, -1)) {
+        result = false;
+    } else {
+        // First, compare the input and Lua array sizes.
+        const auto kActualLength = lua_rawlen(luaState, -1);
+        if (arrayLength != kActualLength) {
+            // No need to compare further if number of elements in the two arrays are not equal.
+            result = false;
+        } else {
+            // Do element by element comparison.
+            bool is_equal = true;
+            for (int i = 0; i < arrayLength; ++i) {
+                lua_rawgeti(luaState, -1, i + 1);
+                if (!lua_isinteger(luaState, /* index = */ -1) ||
+                    (lua_tointeger(luaState, /* index = */ -1) != rawInputArray[i])) {
+                    is_equal = false;
+                }
+                lua_pop(luaState, 1);
+                if (!is_equal) break;
+            }
+            result = is_equal;
+        }
+    }
+    lua_pop(luaState, 1);
+    return result;
+}
+
+extern "C" {
+
+#include "lua.h"
+
+JNIEXPORT jlong JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeCreateLuaEngine(
+        JNIEnv* env, jobject object) {
+    // Cast first to intptr_t to ensure int can hold the pointer without loss.
+    return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
+}
+
+JNIEXPORT void JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeDestroyLuaEngine(
+        JNIEnv* env, jobject object, jlong luaEnginePtr) {
+    delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+}
+
+JNIEXPORT void JNICALL
+Java_com_android_car_scriptexecutor_JniUtilsTest_nativePushBundleToLuaTableCaller(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jobject bundle) {
+    pushBundleToLuaTable(env, reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr)),
+                         bundle);
+}
+
+JNIEXPORT jint JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeGetObjectSize(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jint index) {
+    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+    return lua_rawlen(engine->getLuaState(), static_cast<int>(index));
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasBooleanValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jboolean value) {
+    const char* rawKey = env->GetStringUTFChars(key, nullptr);
+    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+    auto* luaState = engine->getLuaState();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_isboolean(luaState, -1))
+        result = false;
+    else
+        result = static_cast<bool>(lua_toboolean(luaState, -1)) == static_cast<bool>(value);
+    lua_pop(luaState, 1);
+    return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasIntValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jint value) {
+    const char* rawKey = env->GetStringUTFChars(key, nullptr);
+    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+    // Assumes the table is on top of the stack.
+    auto* luaState = engine->getLuaState();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_isinteger(luaState, -1))
+        result = false;
+    else
+        result = lua_tointeger(luaState, -1) == static_cast<int>(value);
+    lua_pop(luaState, 1);
+    return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasDoubleValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jdouble value) {
+    const char* rawKey = env->GetStringUTFChars(key, nullptr);
+    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+    // Assumes the table is on top of the stack.
+    auto* luaState = engine->getLuaState();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_isnumber(luaState, -1))
+        result = false;
+    else
+        result = static_cast<double>(lua_tonumber(luaState, -1)) == static_cast<double>(value);
+    lua_pop(luaState, 1);
+    return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasStringValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jstring value) {
+    const char* rawKey = env->GetStringUTFChars(key, nullptr);
+    LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+    // Assumes the table is on top of the stack.
+    auto* luaState = engine->getLuaState();
+    lua_pushstring(luaState, rawKey);
+    env->ReleaseStringUTFChars(key, rawKey);
+    lua_gettable(luaState, -2);
+    bool result = false;
+    if (!lua_isstring(luaState, -1)) {
+        result = false;
+    } else {
+        std::string s = lua_tostring(luaState, -1);
+        const char* rawValue = env->GetStringUTFChars(value, nullptr);
+        result = strcmp(lua_tostring(luaState, -1), rawValue) == 0;
+        env->ReleaseStringUTFChars(value, rawValue);
+    }
+    lua_pop(luaState, 1);
+    return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasIntArrayValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jintArray value) {
+    jint* rawInputArray = env->GetIntArrayElements(value, nullptr);
+    const auto kInputLength = env->GetArrayLength(value);
+    bool result = hasIntegerArray(env, object, luaEnginePtr, key, rawInputArray, kInputLength);
+    env->ReleaseIntArrayElements(value, rawInputArray, JNI_ABORT);
+    return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasLongArrayValue(
+        JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jlongArray value) {
+    jlong* rawInputArray = env->GetLongArrayElements(value, nullptr);
+    const auto kInputLength = env->GetArrayLength(value);
+    bool result = hasIntegerArray(env, object, luaEnginePtr, key, rawInputArray, kInputLength);
+    env->ReleaseLongArrayElements(value, rawInputArray, JNI_ABORT);
+    return result;
+}
+
+}  //  extern "C"
+
+}  // namespace
+}  // namespace scriptexecutor
+}  // namespace car
+}  // namespace android
+}  // namespace com
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
new file mode 100644
index 0000000..cb76a75
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/ScriptExecutorTest.java
@@ -0,0 +1,738 @@
+/*
+ * 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.scriptexecutor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+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;
+import org.junit.runners.JUnit4;
+
+import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public final class ScriptExecutorTest {
+
+    private IScriptExecutor mScriptExecutor;
+    private ScriptExecutor mInstance;
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+
+    private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
+        public PersistableBundle mSavedBundle;
+        public PersistableBundle mFinalResult;
+        public int mErrorType;
+        public String mMessage;
+        public String mStackTrace;
+        public final CountDownLatch mResponseLatch = new CountDownLatch(1);
+
+        @Override
+        public void onScriptFinished(PersistableBundle result) {
+            mFinalResult = result;
+            mResponseLatch.countDown();
+        }
+
+        @Override
+        public void onSuccess(PersistableBundle stateToPersist) {
+            mSavedBundle = stateToPersist;
+            mResponseLatch.countDown();
+        }
+
+        @Override
+        public void onError(int errorType, String message, String stackTrace) {
+            mErrorType = errorType;
+            mMessage = message;
+            mStackTrace = stackTrace;
+            mResponseLatch.countDown();
+        }
+    }
+
+    private final ScriptExecutorListener mFakeScriptExecutorListener =
+            new ScriptExecutorListener();
+
+    // TODO(b/189241508). Parsing of publishedData is not implemented yet.
+    // Null is the only accepted input.
+    private final PersistableBundle mPublishedData = null;
+    private final PersistableBundle mSavedState = new PersistableBundle();
+
+    private static final String LUA_SCRIPT =
+            "function hello(state)\n"
+                    + "    print(\"Hello World\")\n"
+                    + "end\n";
+
+    private static final String LUA_METHOD = "hello";
+
+    private final CountDownLatch mBindLatch = new CountDownLatch(1);
+
+    private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
+    private static final int SCRIPT_PROCESSING_TIMEOUT_SEC = 10;
+
+
+    private final ServiceConnection mScriptExecutorConnection =
+            new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName className, IBinder service) {
+                    mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
+                    mBindLatch.countDown();
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName className) {
+                    fail("Service unexpectedly disconnected");
+                }
+            };
+
+    // 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.mResponseLatch.await(SCRIPT_PROCESSING_TIMEOUT_SEC,
+                    TimeUnit.SECONDS)) {
+                fail("Failed to get the callback method called by the script on time");
+            }
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+    }
+
+    private void runScriptAndWaitForError(String script, String function) throws RemoteException {
+        runScriptAndWaitForResponse(script, function, new PersistableBundle());
+    }
+
+    @Before
+    public void setUp() throws InterruptedException {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName("com.android.car.scriptexecutor",
+                "com.android.car.scriptexecutor.ScriptExecutor"));
+        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");
+        }
+    }
+
+    @Test
+    public void invokeScript_helloWorld() throws RemoteException {
+        // Expect to load "hello world" script successfully and push the function.
+        mScriptExecutor.invokeScript(LUA_SCRIPT, LUA_METHOD, mPublishedData, mSavedState,
+                mFakeScriptExecutorListener);
+        // Sleep, otherwise the test case will complete before the script loads
+        // because invokeScript is non-blocking.
+        try {
+            // TODO(b/192285332): Replace sleep logic with waiting for specific callbacks
+            // to be called once they are implemented. Otherwise, this could be a flaky test.
+            TimeUnit.SECONDS.sleep(10);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+    }
+
+    @Test
+    public void invokeScript_returnsResult() throws RemoteException {
+        String returnResultScript =
+                "function hello(state)\n"
+                        + "    result = {hello=\"world\"}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResponse(returnResultScript, "hello", mSavedState);
+
+        // Expect to get back a bundle with a single string key: string value pair:
+        // {"hello": "world"}
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("hello")).isEqualTo(
+                "world");
+    }
+
+    @Test
+    public void invokeScript_allSupportedPrimitiveTypes() throws RemoteException {
+        String script =
+                "function knows(state)\n"
+                        + "    result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        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);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo(
+                "hello");
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("integer")).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(1.1);
+    }
+
+    @Test
+    public void invokeScript_skipsUnsupportedTypes() throws RemoteException {
+        String script =
+                "function nested(state)\n"
+                        + "    result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+                        + "    result.nested_table = {x=0, y=0}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResponse(script, "nested", mSavedState);
+
+        // Verify that expected error is received.
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).contains(
+                "nested tables are not supported");
+    }
+
+    @Test
+    public void invokeScript_emptyBundle() throws RemoteException {
+        String script =
+                "function empty(state)\n"
+                        + "    result = {}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+
+        runScriptAndWaitForResponse(script, "empty", mSavedState);
+
+        // If a script returns empty table as the result, we get an empty bundle.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle).isNotNull();
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void invokeScript_processPreviousStateAndReturnResult() throws RemoteException {
+        // Here we verify that the script actually processes provided state from a previous run
+        // and makes calculation based on that and returns the result.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function update(state)\n"
+                        + "    result = {y = state.x+1}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+        PersistableBundle previousState = new PersistableBundle();
+        previousState.putInt("x", 1);
+
+
+        runScriptAndWaitForResponse(script, "update", previousState);
+
+        // Verify that y = 2, because y = x + 1 and x = 1.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("y")).isEqualTo(2);
+    }
+
+    @Test
+    public void invokeScript_allSupportedPrimitiveTypesWorkRoundTripWithKeyNamesPreserved()
+            throws RemoteException {
+        // Here we verify that all supported primitive types in supplied previous state Bundle
+        // are interpreted by the script as expected.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function update_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_success(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, "update_all", previousState);
+
+        // Verify that keys are preserved but the values are modified as expected.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("integer")).isEqualTo(2);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(0.2);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo(
+                "ABRACADABRA");
+    }
+
+    @Test
+    public void invokeScript_allSupportedArrayTypesWorkRoundTripWithKeyNamesPreserved()
+            throws RemoteException {
+        // Here we verify that all supported array types in supplied previous state Bundle are
+        // interpreted by the script as expected.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function arrays(state)\n"
+                        + "    result = {}\n"
+                        + "    result.int_array = state.int_array\n"
+                        + "    result.long_array = state.long_array\n"
+                        + "    result.string_array = state.string_array\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+        PersistableBundle previousState = new PersistableBundle();
+        int[] int_array = new int[]{1, 2};
+        long[] int_array_in_long = new long[]{1, 2};
+        long[] long_array = new long[]{1, 2, 3};
+        String[] string_array = new String[]{"one", "two", "three"};
+        previousState.putIntArray("int_array", int_array);
+        previousState.putLongArray("long_array", long_array);
+        previousState.putStringArray("string_array", string_array);
+
+
+        runScriptAndWaitForResponse(script, "arrays", previousState);
+
+        // Verify that keys are preserved but the values are modified as expected.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(3);
+        // Lua has only one lua_Integer. Here Java long is used to represent it when data is
+        // transferred from Lua to CarTelemetryService.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("int_array")).isEqualTo(
+                int_array_in_long);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("long_array")).isEqualTo(
+                long_array);
+        assertThat(
+                mFakeScriptExecutorListener.mSavedBundle.getStringArray("string_array")).isEqualTo(
+                string_array);
+    }
+
+    @Test
+    public void invokeScript_modifiesArray()
+            throws RemoteException {
+        // Verify that an array modified by a script is properly sent back by the callback.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function modify_array(state)\n"
+                        + "    result = {}\n"
+                        + "    result.long_array = state.long_array\n"
+                        + "    result.long_array[2] = 100\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+        PersistableBundle previousState = new PersistableBundle();
+        long[] long_array = new long[]{1, 2, 3};
+        previousState.putLongArray("long_array", long_array);
+        long[] expected_array = new long[]{1, 100, 3};
+
+
+        runScriptAndWaitForResponse(script, "modify_array", previousState);
+
+        // Verify that keys are preserved but the values are modified as expected.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("long_array")).isEqualTo(
+                expected_array);
+    }
+
+    @Test
+    public void invokeScript_processesStringArray()
+            throws RemoteException {
+        // Verify that an array modified by a script is properly sent back by the callback.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function process_string_array(state)\n"
+                        + "    result = {}\n"
+                        + "    result.answer = state.string_array[1] .. state.string_array[2]\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+        PersistableBundle previousState = new PersistableBundle();
+        String[] string_array = new String[]{"Hello ", "world!"};
+        previousState.putStringArray("string_array", string_array);
+
+        runScriptAndWaitForResponse(script, "process_string_array", previousState);
+
+        // Verify that keys are preserved but the values are modified as expected.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("answer")).isEqualTo(
+                "Hello world!");
+    }
+
+    @Test
+    public void invokeScript_arraysWithLengthAboveLimitCauseError()
+            throws RemoteException {
+        // Verifies that arrays pushed by Lua that have their size over the limit cause error.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function size_limit(state)\n"
+                        + "    result = {}\n"
+                        + "    result.huge_array = {}\n"
+                        + "    for i=1, 10000 do\n"
+                        + "        result.huge_array[i]=i\n"
+                        + "    end\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+        runScriptAndWaitForResponse(script, "size_limit", mSavedState);
+
+        // Verify that expected error is received.
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+                "Returned table huge_array exceeds maximum allowed size of 1000 "
+                        + "elements. This key-value cannot be unpacked successfully. This error "
+                        + "is unrecoverable.");
+    }
+
+    @Test
+    public void invokeScript_arrayContainingVaryingTypesCausesError()
+            throws RemoteException {
+        // Verifies that values in returned array must be the same integer type.
+        // For example string values in a Lua array are not allowed.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function table_with_numbers_and_strings(state)\n"
+                        + "    result = {}\n"
+                        + "    result.mixed_array = state.long_array\n"
+                        + "    result.mixed_array[2] = 'a'\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+        PersistableBundle previousState = new PersistableBundle();
+        long[] long_array = new long[]{1, 2, 3};
+        previousState.putLongArray("long_array", long_array);
+
+        runScriptAndWaitForResponse(script, "table_with_numbers_and_strings", previousState);
+
+        // Verify that expected error is received.
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).contains(
+                "Returned Lua arrays must have elements of the same type.");
+    }
+
+    @Test
+    public void invokeScript_InTablesWithBothKeysAndIndicesCopiesOnlyIndexedData()
+            throws RemoteException {
+        // Documents the current behavior that copies only indexed values in a Lua table that
+        // contains both keyed and indexed data.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function keys_and_indices(state)\n"
+                        + "    result = {}\n"
+                        + "    result.mixed_array = state.long_array\n"
+                        + "    result.mixed_array['a'] = 130\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+        PersistableBundle previousState = new PersistableBundle();
+        long[] long_array = new long[]{1, 2, 3};
+        previousState.putLongArray("long_array", long_array);
+
+        runScriptAndWaitForResponse(script, "keys_and_indices", previousState);
+
+        // Verify that keys are preserved but the values are modified as expected.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("mixed_array")).isEqualTo(
+                long_array);
+    }
+
+    @Test
+    public void invokeScript_noLuaBufferOverflowForLargeInputArrays() throws RemoteException {
+        // Tests that arrays with length that exceed internal Lua buffer size of 20 elements
+        // do not cause buffer overflow and are handled properly.
+        // TODO(b/189241508): update function signatures.
+        String script =
+                "function large_input_array(state)\n"
+                        + "    sum = 0\n"
+                        + "    for _, val in ipairs(state.long_array) do\n"
+                        + "        sum = sum + val\n"
+                        + "    end\n"
+                        + "    result = {total = sum}\n"
+                        + "    on_success(result)\n"
+                        + "end\n";
+
+        PersistableBundle previousState = new PersistableBundle();
+        int n = 10000;
+        long[] longArray = new long[n];
+        for (int i = 0; i < n; i++) {
+            longArray[i] = i;
+        }
+        previousState.putLongArray("long_array", longArray);
+        long expected_sum =
+                (longArray[0] + longArray[n - 1]) * n / 2; // sum of an arithmetic sequence.
+
+        runScriptAndWaitForResponse(script, "large_input_array", previousState);
+
+        // Verify that keys are preserved but the values are modified as expected.
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mSavedBundle.getInt("total")).isEqualTo(
+                expected_sum);
+    }
+
+    @Test
+    public void invokeScript_scriptCallsOnError() throws RemoteException {
+        String script =
+                "function calls_on_error()\n"
+                        + "    if 1 ~= 2 then\n"
+                        + "        on_error(\"one is not equal to two\")\n"
+                        + "        return\n"
+                        + "    end\n"
+                        + "end\n";
+
+        runScriptAndWaitForError(script, "calls_on_error");
+
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo("one is not equal to two");
+    }
+
+    @Test
+    public void invokeScript_tooManyParametersInOnError() throws RemoteException {
+        String script =
+                "function too_many_params_in_on_error()\n"
+                        + "    if 1 ~= 2 then\n"
+                        + "        on_error(\"param1\", \"param2\")\n"
+                        + "        return\n"
+                        + "    end\n"
+                        + "end\n";
+
+        runScriptAndWaitForError(script, "too_many_params_in_on_error");
+
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+                "on_error can push only a single string parameter from Lua");
+    }
+
+    @Test
+    public void invokeScript_onErrorOnlyAcceptsString() throws RemoteException {
+        String script =
+                "function only_string()\n"
+                        + "    if 1 ~= 2 then\n"
+                        + "        on_error(false)\n"
+                        + "        return\n"
+                        + "    end\n"
+                        + "end\n";
+
+        runScriptAndWaitForError(script, "only_string");
+
+        assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+                IScriptExecutorConstants.ERROR_TYPE_LUA_SCRIPT_ERROR);
+        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_allPrimitiveSupportedTypesForReturningFinalResult()
+            throws RemoteException {
+        // Here we verify that all supported primitive 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");
+    }
+
+    @Test
+    public void invokeScriptLargeInput_largePublishedData() throws Exception {
+        // Verifies that large input does not overwhelm Binder's buffer because pipes are used
+        // instead.
+        // TODO(b/189241508): Once publishedData parsing is implemented, change the script and
+        //  the test to process the published data input and return something meaningful.
+        String script =
+                "function large_published_data(state)\n"
+                        + "    result = {success = true}\n"
+                        + "    on_script_finished(result)\n"
+                        + "end\n";
+
+        ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
+        ParcelFileDescriptor writeFd = fds[1];
+        ParcelFileDescriptor readFd = fds[0];
+
+        PersistableBundle bundle = new PersistableBundle();
+        int n = 1 << 20; // 1024 * 1024 values, roughly 1 Million.
+        long[] array8Mb = new long[n];
+        for (int i = 0; i < n; i++) {
+            array8Mb[i] = i;
+        }
+        bundle.putLongArray("array", array8Mb);
+
+        mScriptExecutor.invokeScriptForLargeInput(script, "large_published_data", readFd,
+                mSavedState,
+                mFakeScriptExecutorListener);
+
+        readFd.close();
+        try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeFd)) {
+            bundle.writeToStream(outputStream);
+        }
+
+        boolean gotResponse = mFakeScriptExecutorListener.mResponseLatch.await(
+                SCRIPT_PROCESSING_TIMEOUT_SEC,
+                TimeUnit.SECONDS);
+
+        assertWithMessage("Failed to get the callback method called by the script on time").that(
+                gotResponse).isTrue();
+        assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
+        assertThat(mFakeScriptExecutorListener.mFinalResult.getBoolean("success")).isTrue();
+    }
+}
+
diff --git a/service/Android.bp b/service/Android.bp
index 709c928..7f4a329 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -29,11 +29,13 @@
 }
 
 car_service_sources = [
+    ":iscriptexecutor_aidl",
     "src/**/*.java",
     ":statslog-Car-java-gen",
 ]
 
 common_lib_deps = [
+    "android.automotive.telemetry.internal-java",  // ICarTelemetryInternal
     "android.car.cluster.navigation",
     "android.car.userlib",
     "android.car.watchdoglib",
@@ -85,9 +87,14 @@
 
     jni_libs: [
         "libcarservicejni",
-        "libscriptexecutorjni",
     ],
 
+    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
@@ -138,6 +145,12 @@
 
     static_libs: common_lib_deps,
 
+    aidl: {
+        include_dirs: [
+	    "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+	],
+    },
+
     product_variables: {
         pdk: {
             enabled: false,
@@ -162,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/AndroidManifest.xml b/service/AndroidManifest.xml
index fabe8e2..f8a5e9c 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -936,9 +936,18 @@
         </service>
         <service android:name=".PerUserCarService"
             android:exported="false"/>
-        <service android:name=".telemetry.ScriptExecutor"
-            android:exported="false"
-            android:isolatedProcess="true"/>
+        <service
+            android:name="com.android.car.pm.CarSafetyAccessibilityService"
+            android:singleUser="true"
+            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="android.accessibilityservice.AccessibilityService" />
+            </intent-filter>
+            <meta-data
+                android:name="android.accessibilityservice"
+                android:resource="@xml/car_safety_accessibility_service_config" />
+        </service>
 
         <activity android:name="com.android.car.pm.ActivityBlockingActivity"
              android:documentLaunchMode="always"
diff --git a/service/jni/evs/StreamHandler.cpp b/service/jni/evs/StreamHandler.cpp
index 6f64160..826ff02 100644
--- a/service/jni/evs/StreamHandler.cpp
+++ b/service/jni/evs/StreamHandler.cpp
@@ -161,9 +161,17 @@
         auto it = mReceivedBuffers.begin();
         while (it != mReceivedBuffers.end()) {
             if (it->bufferId == buffer.bufferId) {
+                // We intentionally do not update the iterator to detect a
+                // request to return an unknown buffer.
                 mReceivedBuffers.erase(it);
                 break;
             }
+            ++it;
+        }
+
+        if (it == mReceivedBuffers.end()) {
+            LOG(DEBUG) << "Ignores a request to return unknown buffer";
+            return;
         }
     }
 
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index f0c611c..ae487da 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -413,4 +413,14 @@
     <!-- A configuration flag to adjust Wifi for suspend. -->
     <bool name="config_wifiAdjustmentForSuspend">false</bool>
 
+    <!-- A configuration flag to prevent the templated apps from showing dialogs. This is done in
+         the view of driver safety as templated apps can potentially show a dialog with custom UI
+         which can be a distraction hazard for the driver. -->
+    <bool name="config_preventTemplatedAppsFromShowingDialog">true</bool>
+
+    <!-- The class name of the templated activities. This is used to detect currently running
+         templated activity.-->
+    <string name="config_template_activity_class_name">
+        androidx.car.app.activity.CarAppActivity
+    </string>
 </resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 0709784..416981b 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -584,4 +584,20 @@
     <string name="new_user_managed_device_acceptance" translatable="false">Accept and continue</string>
     <string name="new_user_managed_notification_title" translatable="false">Managed device</string>
 
+    <!-- Title of notification shown when an app overuses system resources [CHAR LIMIT=100] -->
+    <string name="resource_overuse_notification_title"><xliff:g name="app_name" example="Maps">^1</xliff:g> is affecting your system performance</string>
+    <!-- Message of notification shown when an app overuses system resources [CHAR LIMIT=NONE] -->
+    <string name="resource_overuse_notification_text_disable_app">Disable app to improve system performance. You can enable the app once again in Settings.</string>
+    <!-- Message of notification shown when an app overuses system resources [CHAR LIMIT=NONE] -->
+    <string name="resource_overuse_notification_text_prioritize_app">Prioritize app to keep using app.</string>
+    <!-- Message of notification shown when an app overuses system resources [CHAR LIMIT=NONE] -->
+    <string name="resource_overuse_notification_text_uninstall_app">Uninstall app to improve system performance.</string>
+    <!-- Label for button that will disable the app now [CHAR LIMIT=30] -->
+    <string name="resource_overuse_notification_button_disable_app">Disable app</string>
+    <!-- Label for button that will redirect user to prioritize app setting [CHAR LIMIT=30] -->
+    <string name="resource_overuse_notification_button_prioritize_app">Prioritize app</string>
+    <!-- Label for button that will redirect user to uninstall app setting [CHAR LIMIT=30] -->
+    <string name="resource_overuse_notification_button_uninstall_app">Uninstall app</string>
+    <!-- Text of the toast shown when the app is disabled [CHAR_LIMIT=100]-->
+    <string name="resource_overuse_toast_disable_app_now"><xliff:g name="app_name" example="Maps">^1</xliff:g> has been disabled. You can enable it again in Settings.</string>
 </resources>
diff --git a/service/res/xml/car_safety_accessibility_service_config.xml b/service/res/xml/car_safety_accessibility_service_config.xml
new file mode 100644
index 0000000..9029ec2
--- /dev/null
+++ b/service/res/xml/car_safety_accessibility_service_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.
+  -->
+
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accessibilityEventTypes="typeWindowStateChanged"
+    android:accessibilityFlags="flagDefault"
+    android:accessibilityFeedbackType="feedbackAllMask"/>
\ No newline at end of file
diff --git a/service/src/com/android/car/AppFocusService.java b/service/src/com/android/car/AppFocusService.java
index 477529d..6b1c7b4 100644
--- a/service/src/com/android/car/AppFocusService.java
+++ b/service/src/com/android/car/AppFocusService.java
@@ -15,11 +15,14 @@
  */
 package com.android.car;
 
+import android.car.Car;
 import android.car.CarAppFocusManager;
 import android.car.IAppFocus;
 import android.car.IAppFocusListener;
 import android.car.IAppFocusOwnershipCallback;
 import android.content.Context;
+import android.content.PermissionChecker;
+import android.content.pm.PackageManager;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -36,6 +39,7 @@
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -77,9 +81,11 @@
             getClass().getSimpleName());
     private final DispatchHandler mDispatchHandler = new DispatchHandler(mHandlerThread.getLooper(),
             this);
+    private final Context mContext;
 
     public AppFocusService(Context context,
             SystemActivityMonitoringService systemActivityMonitoringService) {
+        mContext = context;
         mSystemActivityMonitoringService = systemActivityMonitoringService;
         mAllChangeClients = new ClientHolder(mAllBinderEventHandler);
         mAllOwnershipClients = new OwnershipClientHolder(this);
@@ -121,6 +127,22 @@
     }
 
     @Override
+    public List<String> getAppTypeOwner(@CarAppFocusManager.AppFocusType int appType) {
+        OwnershipClientInfo owner;
+        synchronized (mLock) {
+            owner = mFocusOwners.get(appType);
+        }
+        if (owner == null) {
+            return null;
+        }
+        String[] packageNames = mContext.getPackageManager().getPackagesForUid(owner.getUid());
+        if (packageNames == null) {
+            return null;
+        }
+        return Arrays.asList(packageNames);
+    }
+
+    @Override
     public boolean isOwningFocus(IAppFocusOwnershipCallback callback, int appType) {
         OwnershipClientInfo info;
         synchronized (mLock) {
@@ -146,10 +168,10 @@
             if (!alreadyOwnedAppTypes.contains(appType)) {
                 OwnershipClientInfo ownerInfo = mFocusOwners.get(appType);
                 if (ownerInfo != null && ownerInfo != info) {
-                    if (mSystemActivityMonitoringService.isInForeground(
-                            ownerInfo.getPid(), ownerInfo.getUid())
-                            && !mSystemActivityMonitoringService.isInForeground(
-                            info.getPid(), info.getUid())) {
+                    // Allow receiving focus if the requester has a foreground activity OR if the
+                    // requester is privileged service.
+                    if (isInForeground(ownerInfo) && !isInForeground(info)
+                            && !hasPrivilegedPermission()) {
                         Slog.w(CarLog.TAG_APP_FOCUS, "Focus request failed for non-foreground app("
                                 + "pid=" + info.getPid() + ", uid=" + info.getUid() + ")."
                                 + "Foreground app (pid=" + ownerInfo.getPid() + ", uid="
@@ -190,6 +212,15 @@
         return CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED;
     }
 
+    private boolean isInForeground(OwnershipClientInfo info) {
+        return mSystemActivityMonitoringService.isInForeground(info.getPid(), info.getUid());
+    }
+
+    private boolean hasPrivilegedPermission() {
+        return mContext.checkCallingOrSelfPermission(Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER)
+                == PermissionChecker.PERMISSION_GRANTED;
+    }
+
     @Override
     public void abandonAppFocus(IAppFocusOwnershipCallback callback, int appType) {
         synchronized (mLock) {
diff --git a/service/src/com/android/car/CarInputService.java b/service/src/com/android/car/CarInputService.java
index 5870b32..f63a6e0 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -60,6 +60,7 @@
 import com.android.car.hal.InputHalService;
 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
 import com.android.car.internal.common.UserHelperLite;
+import com.android.car.pm.CarSafetyAccessibilityService;
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -69,6 +70,7 @@
 import com.android.server.utils.Slogf;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collections;
 import java.util.List;
@@ -81,7 +83,8 @@
  */
 public class CarInputService extends ICarInput.Stub
         implements CarServiceBase, InputHalService.InputListener {
-
+    public static final String ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ":";
+    private static final int MAX_RETRIES_FOR_ENABLING_ACCESSIBILITY_SERVICES = 5;
     private static final String TAG = CarLog.TAG_INPUT;
 
     /** An interface to receive {@link KeyEvent}s as they occur. */
@@ -232,7 +235,7 @@
     private final CarUserManager.UserLifecycleListener mUserLifecycleListener = event -> {
         Slogf.d(TAG, "CarInputService.onEvent(%s)", event);
         if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) {
-            updateRotaryServiceSettings(event.getUserId());
+            updateCarAccessibilityServicesSettings(event.getUserId());
         }
     };
 
@@ -327,9 +330,7 @@
                         mBluetoothProfileServiceListener, BluetoothProfile.HEADSET_CLIENT);
             });
         }
-        if (!TextUtils.isEmpty(mRotaryServiceComponentName)) {
-            mUserService.addUserLifecycleListener(mUserLifecycleListener);
-        }
+        mUserService.addUserLifecycleListener(mUserLifecycleListener);
     }
 
     @Override
@@ -344,9 +345,7 @@
                 mBluetoothHeadsetClient = null;
             }
         }
-        if (!TextUtils.isEmpty(mRotaryServiceComponentName)) {
-            mUserService.removeUserLifecycleListener(mUserLifecycleListener);
-        }
+        mUserService.removeUserLifecycleListener(mUserLifecycleListener);
     }
 
     @Override
@@ -450,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,
@@ -458,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) {
@@ -691,6 +703,26 @@
         return true;
     }
 
+    private List<String> getAccessibilityServicesToBeEnabled() {
+        String carSafetyAccessibilityServiceComponentName = mContext.getPackageName()
+                + "/"
+                + CarSafetyAccessibilityService.class.getName();
+        ArrayList<String> accessibilityServicesToBeEnabled = new ArrayList<>();
+        accessibilityServicesToBeEnabled.add(carSafetyAccessibilityServiceComponentName);
+        if (!TextUtils.isEmpty(mRotaryServiceComponentName)) {
+            accessibilityServicesToBeEnabled.add(mRotaryServiceComponentName);
+        }
+        return accessibilityServicesToBeEnabled;
+    }
+
+    private static List<String> createServiceListFromSettingsString(
+            String accessibilityServicesString) {
+        return TextUtils.isEmpty(accessibilityServicesString)
+                ? new ArrayList<>()
+                : Arrays.asList(accessibilityServicesString.split(
+                        ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR));
+    }
+
     @Override
     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
     public void dump(IndentingPrintWriter writer) {
@@ -701,15 +733,44 @@
         mCaptureController.dump(writer);
     }
 
-    private void updateRotaryServiceSettings(@UserIdInt int userId) {
+    private void updateCarAccessibilityServicesSettings(@UserIdInt int userId) {
         if (UserHelperLite.isHeadlessSystemUser(userId)) {
             return;
         }
+        List<String> accessibilityServicesToBeEnabled = getAccessibilityServicesToBeEnabled();
         ContentResolver contentResolver = mContext.getContentResolver();
-        Settings.Secure.putStringForUser(contentResolver,
-                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
-                mRotaryServiceComponentName,
-                userId);
+        List<String> alreadyEnabledServices = createServiceListFromSettingsString(
+                Settings.Secure.getStringForUser(contentResolver,
+                        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                        userId));
+
+        int retry = 0;
+        while (!alreadyEnabledServices.containsAll(accessibilityServicesToBeEnabled)
+                && retry <= MAX_RETRIES_FOR_ENABLING_ACCESSIBILITY_SERVICES) {
+            ArrayList<String> enabledServicesList = new ArrayList<>(alreadyEnabledServices);
+            int numAccessibilityServicesToBeEnabled = accessibilityServicesToBeEnabled.size();
+            for (int i = 0; i < numAccessibilityServicesToBeEnabled; i++) {
+                String serviceToBeEnabled = accessibilityServicesToBeEnabled.get(i);
+                if (!enabledServicesList.contains(serviceToBeEnabled)) {
+                    enabledServicesList.add(serviceToBeEnabled);
+                }
+            }
+            Settings.Secure.putStringForUser(contentResolver,
+                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                    String.join(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR, enabledServicesList),
+                    userId);
+            // Read again to account for any race condition with other parts of the code that might
+            // be enabling other accessibility services.
+            alreadyEnabledServices = createServiceListFromSettingsString(
+                    Settings.Secure.getStringForUser(contentResolver,
+                            Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                            userId));
+            retry++;
+        }
+        if (!alreadyEnabledServices.containsAll(accessibilityServicesToBeEnabled)) {
+            Slogf.e(TAG, "Failed to enable accessibility services");
+        }
+
         Settings.Secure.putStringForUser(contentResolver,
                 Settings.Secure.ACCESSIBILITY_ENABLED,
                 "1",
diff --git a/service/src/com/android/car/CarMediaService.java b/service/src/com/android/car/CarMediaService.java
index 8cee561..1f6375e 100644
--- a/service/src/com/android/car/CarMediaService.java
+++ b/service/src/com/android/car/CarMediaService.java
@@ -21,6 +21,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
+import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.car.Car;
 import android.car.hardware.power.CarPowerPolicy;
@@ -60,6 +61,7 @@
 import android.os.UserManager;
 import android.service.media.MediaBrowserService;
 import android.text.TextUtils;
+import android.util.DebugUtils;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
@@ -68,6 +70,7 @@
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -76,6 +79,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.stream.Collectors;
 
 /**
@@ -87,7 +91,9 @@
  * it were being browsed only. However, that source is still considered the active source, and
  * should be the source displayed in any Media related UIs (Media Center, home screen, etc).
  */
-public class CarMediaService extends ICarMedia.Stub implements CarServiceBase {
+public final class CarMediaService extends ICarMedia.Stub implements CarServiceBase {
+
+    private static final boolean DEBUG = false;
 
     private static final String SOURCE_KEY = "media_source_component";
     private static final String SOURCE_KEY_SEPARATOR = "_";
@@ -96,6 +102,7 @@
     private static final String COMPONENT_NAME_SEPARATOR = ",";
     private static final String MEDIA_CONNECTION_ACTION = "com.android.car.media.MEDIA_CONNECTION";
     private static final String EXTRA_AUTOPLAY = "com.android.car.media.autoplay";
+    private static final String LAST_UPDATE_KEY = "last_update";
 
     private static final int MEDIA_SOURCE_MODES = 2;
 
@@ -126,6 +133,8 @@
     private boolean mWasPreviouslyDisabledByPowerPolicy;
     @GuardedBy("mLock")
     private boolean mWasPlayingBeforeDisabled;
+
+    // NOTE: must use getSharedPrefsForWriting() to write to it
     private SharedPreferences mSharedPrefs;
     private SessionChangedListener mSessionsListener;
     private int mPlayOnMediaSourceChangedConfig;
@@ -151,6 +160,7 @@
     private ComponentName[] mRemovedMediaSourceComponents = new ComponentName[MEDIA_SOURCE_MODES];
 
     private final IntentFilter mPackageUpdateFilter;
+    @GuardedBy("mLock")
     private boolean mIsPackageUpdateReceiverRegistered;
 
     /**
@@ -230,7 +240,7 @@
                 maybeInitUser(event.getUserId());
                 break;
             case CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED:
-                onUserUnlock(event.getUserId());
+                onUserUnlocked(event.getUserId());
                 break;
         }
     };
@@ -309,8 +319,9 @@
     @Override
     // This method is called from ICarImpl after CarMediaService is created.
     public void init() {
-        int currentUser = ActivityManager.getCurrentUser();
-        maybeInitUser(currentUser);
+        int currentUserId = ActivityManager.getCurrentUser();
+        Slog.d(CarLog.TAG_MEDIA, "init(): currentUser=" + currentUserId);
+        maybeInitUser(currentUserId);
         setPowerPolicyListener();
     }
 
@@ -325,21 +336,16 @@
         }
     }
 
-    private void initUser(int userId) {
-        // SharedPreferences are shared among different users thus only need initialized once. And
-        // they should be initialized after user 0 is unlocked because SharedPreferences in
-        // credential encrypted storage are not available until after user 0 is unlocked.
-        // initUser() is called when the current foreground user is unlocked, and by that time user
-        // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine.
-        synchronized (mLock) {
-            if (mSharedPrefs == null) {
-                mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
-            }
+    private void initUser(@UserIdInt int userId) {
+        Slog.d(CarLog.TAG_MEDIA, "initUser(): userId=" + userId + ", mSharedPrefs=" + mSharedPrefs);
+        UserHandle currentUser = new UserHandle(userId);
 
+        maybeInitSharedPrefs(userId);
+
+        synchronized (mLock) {
             if (mIsPackageUpdateReceiverRegistered) {
                 mContext.unregisterReceiver(mPackageUpdateReceiver);
             }
-            UserHandle currentUser = new UserHandle(userId);
             mContext.registerReceiverAsUser(mPackageUpdateReceiver, currentUser,
                     mPackageUpdateFilter, null, null);
             mIsPackageUpdateReceiverRegistered = true;
@@ -358,6 +364,30 @@
         }
     }
 
+    private void maybeInitSharedPrefs(@UserIdInt int userId) {
+        // SharedPreferences are shared among different users thus only need initialized once. And
+        // they should be initialized after user 0 is unlocked because SharedPreferences in
+        // credential encrypted storage are not available until after user 0 is unlocked.
+        // initUser() is called when the current foreground user is unlocked, and by that time user
+        // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine.
+        if (mSharedPrefs != null) {
+            Slog.i(CarLog.TAG_MEDIA, "Shared preferences already set (on directory "
+                    + mContext.getDataDir() + ") when initializing user " + userId);
+            return;
+        }
+        Slog.i(CarLog.TAG_MEDIA, "Getting shared preferences when initializing user "
+                + userId);
+        mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
+
+        // Try to access the properties to make sure they were properly open
+        if (DEBUG) {
+            Slogf.i(CarLog.TAG_MEDIA, "Number of prefs: %d", mSharedPrefs.getAll().size());
+
+        } else if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
+            Slogf.d(CarLog.TAG_MEDIA, "Number of prefs: %d", mSharedPrefs.getAll().size());
+        }
+    }
+
     /**
      * Starts a service on the current user that binds to the media browser of the current media
      * source. We start a new service because this one runs on user 0, and MediaBrowser doesn't
@@ -373,20 +403,19 @@
     }
 
     private boolean sharedPrefsInitialized() {
-        if (mSharedPrefs == null) {
-            // It shouldn't reach this but let's be cautious.
-            Slog.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!");
-            String className = getClass().getName();
-            for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
-                // Let's print the useful logs only.
-                String log = ste.toString();
-                if (log.contains(className)) {
-                    Slog.e(CarLog.TAG_MEDIA, log);
-                }
+        if (mSharedPrefs != null) return true;
+
+        // It shouldn't reach this but let's be cautious.
+        Slog.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!");
+        String className = getClass().getName();
+        for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
+            // Let's print the useful logs only.
+            String log = ste.toString();
+            if (log.contains(className)) {
+                Slog.e(CarLog.TAG_MEDIA, log);
             }
-            return false;
         }
-        return true;
+        return false;
     }
 
     private boolean isCurrentUserEphemeral() {
@@ -414,43 +443,85 @@
 
     @Override
     public void dump(IndentingPrintWriter writer) {
+        writer.println("*CarMediaService*");
+        writer.increaseIndent();
+
+        writer.printf("Pending init: %b\n", mPendingInit);
+        boolean hasSharedPrefs;
         synchronized (mLock) {
-            writer.println("*CarMediaService*");
-            writer.increaseIndent();
-            writer.printf("Current playback media component: %s\n",
-                    mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null ? "-"
-                    : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK].flattenToString());
-            writer.printf("Current browse media component: %s\n",
-                    mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] == null ? "-"
-                    : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE].flattenToString());
+            hasSharedPrefs = mSharedPrefs != null;
+            dumpCurrentMediaComponent(writer, "playback", MEDIA_SOURCE_MODE_PLAYBACK);
+            dumpCurrentMediaComponent(writer, "browse", MEDIA_SOURCE_MODE_BROWSE);
             if (mActiveUserMediaController != null) {
                 writer.printf("Current media controller: %s\n",
                         mActiveUserMediaController.getPackageName());
                 writer.printf("Current browse service extra: %s\n",
                         getClassName(mActiveUserMediaController));
+            } else {
+                writer.println("no active user media controller");
             }
-            writer.printf("Number of active media sessions: %s\n", mMediaSessionManager
-                    .getActiveSessionsForUser(null,
-                            new UserHandle(ActivityManager.getCurrentUser())).size());
+            int userId = ActivityManager.getCurrentUser();
+            writer.printf("Number of active media sessions (for current user %d): %d\n", userId,
+                    mMediaSessionManager.getActiveSessionsForUser(/* notificationListener= */ null,
+                            new UserHandle(userId)).size());
 
-            writer.println("Playback media source history:");
-            writer.increaseIndent();
-            for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_PLAYBACK)) {
-                writer.println(name.flattenToString());
-            }
-            writer.decreaseIndent();
-            writer.println("Browse media source history:");
-            writer.increaseIndent();
-            for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_BROWSE)) {
-                writer.println(name.flattenToString());
-            }
-            writer.decreaseIndent();
             writer.printf("Disabled by power policy: %s\n", mIsDisabledByPowerPolicy);
             if (mIsDisabledByPowerPolicy) {
                 writer.printf("Before being disabled by power policy, audio was %s\n",
                         mWasPlayingBeforeDisabled ? "active" : "inactive");
             }
         }
+
+        if (hasSharedPrefs) {
+            dumpLastMediaSources(writer, "Playback", MEDIA_SOURCE_MODE_PLAYBACK);
+            dumpLastMediaSources(writer, "Browse", MEDIA_SOURCE_MODE_BROWSE);
+            dumpSharedPrefs(writer);
+        } else {
+            writer.println("No shared preferences");
+        }
+
+        writer.decreaseIndent();
+    }
+
+    private void dumpCurrentMediaComponent(IndentingPrintWriter writer, String name,
+            @CarMediaManager.MediaSourceMode int mode) {
+        ComponentName componentName = mPrimaryMediaComponents[mode];
+        writer.printf("Current %s media component: %s\n", name, componentName == null
+                ? "-"
+                : componentName.flattenToString());
+    }
+
+    private void dumpLastMediaSources(IndentingPrintWriter writer, String name,
+            @CarMediaManager.MediaSourceMode int mode) {
+        writer.printf("%s media source history:\n", name);
+        writer.increaseIndent();
+        List<ComponentName> lastMediaSources = getLastMediaSources(mode);
+        for (int i = 0; i < lastMediaSources.size(); i++) {
+            ComponentName componentName = lastMediaSources.get(i);
+            if (componentName == null) {
+                Slogf.e(CarLog.TAG_MEDIA, "dump(): empty last media source of %s at index %d: %s",
+                        mediaModeToString(mode), i, lastMediaSources);
+                continue;
+            }
+            writer.println(componentName.flattenToString());
+        }
+        writer.decreaseIndent();
+    }
+
+    private void dumpSharedPrefs(IndentingPrintWriter writer) {
+        Map<String, ?> allPrefs = mSharedPrefs.getAll();
+        writer.printf("%d shared preferences (saved on directory %s)",
+                allPrefs.size(), mContext.getDataDir());
+        if (!Log.isLoggable(CarLog.TAG_MEDIA, Log.VERBOSE) || allPrefs.isEmpty()) {
+            writer.println();
+            return;
+        }
+        writer.println(':');
+        writer.increaseIndent();
+        for (Entry<String, ?> pref : allPrefs.entrySet()) {
+            writer.printf("%s = %s\n", pref.getKey(), pref.getValue());
+        }
+        writer.decreaseIndent();
     }
 
     /**
@@ -531,10 +602,12 @@
     }
 
     // TODO(b/153115826): this method was used to be called from the ICar binder thread, but it's
-    // now called by UserCarService. Currently UserCarServie is calling every listener in one
+    // now called by UserCarService. Currently UserCarService is calling every listener in one
     // non-main thread, but it's not clear how the final behavior will be. So, for now it's ok
     // to post it to mMainHandler, but once b/145689885 is fixed, we might not need it.
-    private void onUserUnlock(int userId) {
+    private void onUserUnlocked(@UserIdInt int userId) {
+        Slog.d(CarLog.TAG_MEDIA, "onUserUnlocked(): userId=" + userId
+                + ", mPendingInit=" + mPendingInit);
         mMainHandler.post(() -> {
             // No need to handle system user, non current foreground user.
             if (userId == UserHandle.USER_SYSTEM
@@ -904,13 +977,28 @@
         String componentName = component.flattenToString();
         String key = getMediaSourceKey(mode);
         String serialized = mSharedPrefs.getString(key, null);
+        String modeName = null;
+        boolean debug = DEBUG || Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG);
+        if (debug) {
+            modeName = mediaModeToString(mode);
+        }
+
         if (serialized == null) {
-            mSharedPrefs.edit().putString(key, componentName).apply();
+            if (debug) {
+                Slogf.d(CarLog.TAG_MEDIA, "saveLastMediaSource(%s, %s): no value for key %s",
+                        componentName, modeName, key);
+            }
+            getSharedPrefsForWriting().putString(key, componentName).apply();
         } else {
             Deque<String> componentNames = new ArrayDeque<>(getComponentNameList(serialized));
             componentNames.remove(componentName);
             componentNames.addFirst(componentName);
-            mSharedPrefs.edit().putString(key, serializeComponentNameList(componentNames)).apply();
+            String newSerialized = serializeComponentNameList(componentNames);
+            if (debug) {
+                Slogf.d(CarLog.TAG_MEDIA, "saveLastMediaSource(%s, %s): updating %s from %s to %s",
+                        componentName, modeName,  key, serialized, newSerialized);
+            }
+            getSharedPrefsForWriting().putString(key, newSerialized).apply();
         }
     }
 
@@ -960,7 +1048,8 @@
             mCurrentPlaybackState = state;
         }
         String key = getPlaybackStateKey();
-        mSharedPrefs.edit().putInt(key, state).apply();
+        Slogf.d(CarLog.TAG_MEDIA, "savePlaybackState(): %s = %d)", key, state);
+        getSharedPrefsForWriting().putInt(key, state).apply();
     }
 
     /**
@@ -1031,6 +1120,15 @@
         }
     }
 
+    /**
+     * Gets the editor used to update shared preferences.
+     */
+    private SharedPreferences.Editor getSharedPrefsForWriting() {
+        long now = System.currentTimeMillis();
+        Slogf.i(CarLog.TAG_MEDIA, "Updating %s to %d", LAST_UPDATE_KEY, now);
+        return mSharedPrefs.edit().putLong(LAST_UPDATE_KEY, now);
+    }
+
     @NonNull
     private static String getClassName(@NonNull MediaController controller) {
         Bundle sessionExtras = controller.getExtras();
@@ -1039,4 +1137,8 @@
                         Car.CAR_EXTRA_BROWSE_SERVICE_FOR_SESSION);
         return value != null ? value : "";
     }
+
+    private static String mediaModeToString(@CarMediaManager.MediaSourceMode int mode) {
+        return DebugUtils.constantToString(CarMediaManager.class, "MEDIA_SOURCE_", mode);
+    }
 }
diff --git a/service/src/com/android/car/CarShellCommand.java b/service/src/com/android/car/CarShellCommand.java
index 083eeb8..f1fec38 100644
--- a/service/src/com/android/car/CarShellCommand.java
+++ b/service/src/com/android/car/CarShellCommand.java
@@ -18,6 +18,7 @@
 import static android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME;
 import static android.car.Car.PERMISSION_CAR_POWER;
 import static android.car.Car.PERMISSION_CONTROL_CAR_WATCHDOG_CONFIG;
+import static android.car.Car.PERMISSION_USE_CAR_WATCHDOG;
 import static android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue.ASSOCIATE_CURRENT_USER;
 import static android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue.DISASSOCIATE_ALL_USERS;
 import static android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue.DISASSOCIATE_CURRENT_USER;
@@ -172,6 +173,10 @@
     private static final String COMMAND_APPLY_POWER_POLICY = "apply-power-policy";
     private static final String COMMAND_DEFINE_POWER_POLICY_GROUP = "define-power-policy-group";
     private static final String COMMAND_SET_POWER_POLICY_GROUP = "set-power-policy-group";
+    private static final String COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY =
+            "apply-cts-verifier-power-off-policy";
+    private static final String COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY =
+            "apply-cts-verifier-power-on-policy";
     private static final String COMMAND_POWER_OFF = "power-off";
     private static final String POWER_OFF_SKIP_GARAGEMODE = "--skip-garagemode";
     private static final String POWER_OFF_SHUTDOWN = "--shutdown";
@@ -198,6 +203,8 @@
             "watchdog-io-set-3p-foreground-bytes";
     private static final String COMMAND_WATCHDOG_IO_GET_3P_FOREGROUND_BYTES =
             "watchdog-io-get-3p-foreground-bytes";
+    private static final String COMMAND_WATCHDOG_CONTROL_PROCESS_HEALTH_CHECK =
+            "watchdog-control-health-check";
 
     private static final String COMMAND_DRIVING_SAFETY_SET_REGION =
             "set-drivingsafety-region";
@@ -253,6 +260,10 @@
                 android.Manifest.permission.DEVICE_POWER);
         USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_SET_POWER_POLICY_GROUP,
                 android.Manifest.permission.DEVICE_POWER);
+        USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY,
+                android.Manifest.permission.DEVICE_POWER);
+        USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY,
+                android.Manifest.permission.DEVICE_POWER);
         USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_SILENT_MODE,
                 PERMISSION_CAR_POWER);
         USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_GET_INITIAL_USER,
@@ -273,6 +284,8 @@
                 PERMISSION_CONTROL_CAR_WATCHDOG_CONFIG);
         USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_WATCHDOG_IO_GET_3P_FOREGROUND_BYTES,
                 PERMISSION_CONTROL_CAR_WATCHDOG_CONFIG);
+        USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_WATCHDOG_CONTROL_PROCESS_HEALTH_CHECK,
+                PERMISSION_USE_CAR_WATCHDOG);
     }
 
     private static final String PARAM_DAY_MODE = "day";
@@ -598,6 +611,14 @@
         pw.println("\t  Sets power policy group which is defined in /vendor/etc/power_policy.xml ");
         pw.printf("\t  or by %s command\n", COMMAND_DEFINE_POWER_POLICY_GROUP);
 
+        pw.printf("\t%s\n", COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY);
+        pw.println("\t  Define and apply the cts_verifier_off power policy with "
+                + "--disable WIFI,LOCATION,BLUETOOTH");
+
+        pw.printf("\t%s\n", COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY);
+        pw.println("\t  Define and apply the cts_verifier_on power policy with "
+                + "--enable WIFI,LOCATION,BLUETOOTH");
+
         pw.printf("\t%s [%s] [%s]\n", COMMAND_POWER_OFF, POWER_OFF_SKIP_GARAGEMODE,
                 POWER_OFF_SHUTDOWN);
         pw.println("\t  Powers off the car.");
@@ -617,6 +638,10 @@
         pw.printf("\t%s\n", COMMAND_WATCHDOG_IO_GET_3P_FOREGROUND_BYTES);
         pw.println("\t  Gets third-party apps foreground I/O overuse threshold");
 
+        pw.printf("\t%s enable|disable\n", COMMAND_WATCHDOG_CONTROL_PROCESS_HEALTH_CHECK);
+        pw.println("\t  Enables/disables car watchdog process health check.");
+        pw.println("\t  Set to true to disable the process health check.");
+
         pw.printf("\t%s [REGION_STRING]", COMMAND_DRIVING_SAFETY_SET_REGION);
         pw.println("\t  Set driving safety region.");
         pw.println("\t  Skipping REGION_STRING leads into resetting to all regions");
@@ -933,6 +958,10 @@
                 return definePowerPolicyGroup(args, writer);
             case COMMAND_SET_POWER_POLICY_GROUP:
                 return setPowerPolicyGroup(args, writer);
+            case COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY:
+                return applyCtsVerifierPowerOffPolicy(args, writer);
+            case COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY:
+                return applyCtsVerifierPowerOnPolicy(args, writer);
             case COMMAND_POWER_OFF:
                 powerOff(args, writer);
                 break;
@@ -948,6 +977,9 @@
             case COMMAND_WATCHDOG_IO_GET_3P_FOREGROUND_BYTES:
                 getWatchdogIoThirdPartyForegroundBytes(writer);
                 break;
+            case COMMAND_WATCHDOG_CONTROL_PROCESS_HEALTH_CHECK:
+                controlWatchdogProcessHealthCheck(args, writer);
+                break;
             case COMMAND_DRIVING_SAFETY_SET_REGION:
                 setDrivingSafetyRegion(args, writer);
                 break;
@@ -1994,6 +2026,29 @@
         return RESULT_ERROR;
     }
 
+    private int applyCtsVerifierPowerPolicy(String policyId, String ops, String cmdName,
+            IndentingPrintWriter writer) {
+        String[] defArgs = {"define-power-policy", policyId, ops, "WIFI,BLUETOOTH,LOCATION"};
+        mCarPowerManagementService.definePowerPolicyFromCommand(defArgs, writer);
+
+        String[] appArgs = {"apply-power-policy", policyId};
+        boolean result = mCarPowerManagementService.applyPowerPolicyFromCommand(appArgs, writer);
+        if (result) return RESULT_OK;
+
+        writer.printf("\nUsage: cmd car_service %s\n", cmdName);
+        return RESULT_ERROR;
+    }
+
+    private int applyCtsVerifierPowerOffPolicy(String[] unusedArgs, IndentingPrintWriter writer) {
+        return applyCtsVerifierPowerPolicy("cts_verifier_off", "--disable",
+                COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY, writer);
+    }
+
+    private int applyCtsVerifierPowerOnPolicy(String[] unusedArgs, IndentingPrintWriter writer) {
+        return applyCtsVerifierPowerPolicy("cts_verifier_on", "--enable",
+                COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY, writer);
+    }
+
     private void powerOff(String[] args, IndentingPrintWriter writer) {
         int index = 1;
         boolean skipGarageMode = false;
@@ -2221,6 +2276,19 @@
                 .setIoOveruseConfiguration(configuration.getIoOveruseConfiguration());
     }
 
+    private void controlWatchdogProcessHealthCheck(String[] args, IndentingPrintWriter writer) {
+        if (args.length != 2) {
+            showInvalidArguments(writer);
+            return;
+        }
+        if (!args[1].equals("enable") && !args[1].equals("disable")) {
+            writer.println("Failed to parse argument. Valid arguments: enable | disable");
+            return;
+        }
+        mCarWatchdogService.controlProcessHealthCheck(args[1].equals("disable"));
+        writer.printf("Watchdog health checking is now %sd \n", args[1]);
+    }
+
     // Check if the given property is global
     private static boolean isPropertyAreaTypeGlobal(@Nullable String property) {
         if (property == null) {
diff --git a/service/src/com/android/car/FastPairGattServer.java b/service/src/com/android/car/FastPairGattServer.java
index 6cbaae0..6fc537d 100644
--- a/service/src/com/android/car/FastPairGattServer.java
+++ b/service/src/com/android/car/FastPairGattServer.java
@@ -86,6 +86,7 @@
     private static final boolean DBG = FastPairUtils.DBG;
     private static final int MAX_KEY_COUNT = 10;
     private static final int KEY_LIFESPAN = 10_000;
+    private static final int INVALID = -1;
 
     private final boolean mAutomaticPasskeyConfirmation;
     private final byte[] mModelId;
@@ -95,7 +96,7 @@
     private ArrayList<AccountKey> mKeys = new ArrayList<>();
     private BluetoothGattServer mBluetoothGattServer;
     private BluetoothManager mBluetoothManager;
-    private int mPairingPasskey = -1;
+    private int mPairingPasskey = INVALID;
     private int mFailureCount = 0;
     private int mSuccessCount = 0;
     private BluetoothGattService mFastPairService = new BluetoothGattService(
@@ -149,7 +150,9 @@
         @Override
         public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
             super.onConnectionStateChange(device, status, newState);
-            Log.d(TAG, "onConnectionStateChange " + newState + "Device: " + device.toString());
+            if (DBG) {
+                Log.d(TAG, "onConnectionStateChange " + newState + "Device: " + device.toString());
+            }
             if (newState == 0) {
                 mPairingPasskey = -1;
                 mSharedSecretKey = null;
@@ -249,12 +252,13 @@
                 Log.d(TAG, intent.getAction());
             }
             if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) {
-                if (DBG) {
-                    Log.d(TAG, "PairingCode " + intent
-                                    .getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, -1));
-                }
                 mRemotePairingDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-                mPairingPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, -1);
+                mPairingPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, INVALID);
+                if (DBG) {
+                    Log.d(TAG, "DeviceAddress: " + mRemotePairingDevice
+                            + " PairingCode: " + mPairingPasskey);
+                }
+                sendPairingResponse(mPairingPasskey);
             }
         }
     };
@@ -309,6 +313,9 @@
         } catch (Exception e) {
             Log.e(TAG, e.toString());
         }
+        if (DBG) {
+            Log.w(TAG, "Encryption Failed, clear key");
+        }
         mHandler.removeCallbacks(mClearSharedSecretKey);
         mSharedSecretKey = null;
         return null;
@@ -439,7 +446,7 @@
 
         byte[] encryptedRequest = Arrays.copyOfRange(pairingRequest, 0, 16);
         if (DBG) {
-            Log.d(TAG, "Checking " + possibleKeys.size() + "Keys");
+            Log.d(TAG, "Checking " + possibleKeys.size() + " Keys");
         }
         // check all the keys for a valid pairing request
         for (SecretKeySpec key : possibleKeys) {
@@ -486,7 +493,7 @@
                     .getRemoteDevice(remoteAddressBytes);
             if (DBG) {
                 Log.d(TAG, "Local RPA = " + mLocalRpaDevice);
-                Log.d(TAG, "Decrypted, LocalMacAddress" + localAddress + "remoteAddress"
+                Log.d(TAG, "Decrypted, LocalMacAddress: " + localAddress + " remoteAddress: "
                         + reportedDevice.toString());
             }
             // Test that the received device address matches this devices address
@@ -494,7 +501,6 @@
                 if (DBG) {
                     Log.d(TAG, "SecretKey Validated");
                 }
-
                 // encrypt and respond to the seeker with the local public address
                 byte[] rawResponse = new byte[16];
                 new Random().nextBytes(rawResponse);
@@ -536,16 +542,23 @@
                 }
                 mRemotePairingDevice.setPairingConfirmation(true);
             }
-        } else {
+        } else if (mPairingPasskey != INVALID) {
             Log.w(TAG, "Passkeys don't match, rejecting");
             mRemotePairingDevice.setPairingConfirmation(false);
         }
+        return true;
+    }
 
+    void sendPairingResponse(int passkey) {
+        if (!isConnected()) return;
+        if (DBG) {
+            Log.d(TAG, "sendPairingResponse + " + passkey);
+        }
         // Send an encrypted response to the seeker with the Bluetooth passkey as required
         byte[] decryptedResponse = new byte[16];
         new Random().nextBytes(decryptedResponse);
         ByteBuffer pairingPasskeyBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(
-                mPairingPasskey);
+                passkey);
         decryptedResponse[0] = 0x3;
         decryptedResponse[1] = pairingPasskeyBytes.get(1);
         decryptedResponse[2] = pairingPasskeyBytes.get(2);
@@ -553,11 +566,9 @@
 
         mEncryptedResponse = encrypt(decryptedResponse);
         if (mEncryptedResponse == null) {
-            return false;
+            return;
         }
         mPasskeyCharacteristic.setValue(mEncryptedResponse);
-        return true;
-
     }
 
     /**
diff --git a/service/src/com/android/car/FastPairProvider.java b/service/src/com/android/car/FastPairProvider.java
index dc63058..90529d4 100644
--- a/service/src/com/android/car/FastPairProvider.java
+++ b/service/src/com/android/car/FastPairProvider.java
@@ -18,6 +18,7 @@
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -48,6 +49,7 @@
     private final Context mContext;
     private boolean mStarted;
     private int mScanMode;
+    private BluetoothAdapter mBluetoothAdapter;
     private FastPairAdvertiser mFastPairModelAdvertiser;
     private FastPairAdvertiser mFastPairAccountAdvertiser;
     private FastPairGattServer mFastPairGattServer;
@@ -93,7 +95,11 @@
                         Slog.d(TAG, "NewScanMode = " + mScanMode);
                     }
                     if (mScanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
-                        advertiseModelId();
+                        if (mBluetoothAdapter.isDiscovering()) {
+                            advertiseModelId();
+                        } else {
+                            stopAdvertising();
+                        }
                     } else if (mScanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE
                             && mFastPairGattServer != null
                             && !mFastPairGattServer.isConnected()) {
@@ -130,6 +136,7 @@
         mModelId = res.getInteger(R.integer.fastPairModelId);
         mAntiSpoofKey = res.getString(R.string.fastPairAntiSpoofKey);
         mAutomaticAcceptance = res.getBoolean(R.bool.fastPairAutomaticAcceptance);
+        mBluetoothAdapter = mContext.getSystemService(BluetoothManager.class).getAdapter();
     }
 
     /**
@@ -161,6 +168,20 @@
         }
     }
 
+    void stopAdvertising() {
+        mFastPairAdvertiserHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mFastPairAccountAdvertiser != null) {
+                    mFastPairAccountAdvertiser.stopAdvertising();
+                }
+                if (mFastPairModelAdvertiser != null) {
+                    mFastPairModelAdvertiser.stopAdvertising();
+                }
+            }
+        });
+    }
+
     void advertiseModelId() {
         mFastPairAdvertiserHandler.post(new Runnable() {
             @Override
@@ -195,7 +216,6 @@
                 mFastPairAccountAdvertiser.advertiseAccountKeys();
             }
         });
-
     }
 
     void startGatt() {
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/CarAudioPolicyVolumeCallback.java b/service/src/com/android/car/audio/CarAudioPolicyVolumeCallback.java
index 6fccbf9..14b1a6f 100644
--- a/service/src/com/android/car/audio/CarAudioPolicyVolumeCallback.java
+++ b/service/src/com/android/car/audio/CarAudioPolicyVolumeCallback.java
@@ -31,6 +31,8 @@
 
 import com.android.car.audio.CarAudioContext.AudioContext;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+
 
 import java.util.Objects;
 
@@ -67,25 +69,32 @@
 
         int zoneId = PRIMARY_AUDIO_ZONE;
         int groupId = mCarAudioService.getVolumeGroupIdForAudioContext(zoneId, suggestedContext);
+        boolean isMuted = isMuted(zoneId, groupId);
 
-        if (Log.isLoggable(TAG_AUDIO, VERBOSE)) {
-            Slog.v(TAG_AUDIO, "onVolumeAdjustment: "
+        if (Slogf.isLoggable(TAG_AUDIO, VERBOSE)) {
+            Slogf.v(TAG_AUDIO, "onVolumeAdjustment: "
                     + AudioManager.adjustToString(adjustment) + " suggested audio context: "
                     + CarAudioContext.toString(suggestedContext) + " suggested volume group: "
-                    + groupId);
+                    + groupId + " is muted " + isMuted);
         }
 
         final int currentVolume = mCarAudioService.getGroupVolume(zoneId, groupId);
         final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
+        int minGain = mCarAudioService.getGroupMinVolume(zoneId, groupId);
         switch (adjustment) {
             case AudioManager.ADJUST_LOWER:
-                int minValue = Math.max(currentVolume - 1,
-                        mCarAudioService.getGroupMinVolume(zoneId, groupId));
+                int minValue = Math.max(currentVolume - 1, minGain);
+                if (isMuted)  {
+                    minValue = minGain;
+                }
                 mCarAudioService.setGroupVolume(zoneId, groupId, minValue, flags);
                 break;
             case AudioManager.ADJUST_RAISE:
                 int maxValue = Math.min(currentVolume + 1,
                         mCarAudioService.getGroupMaxVolume(zoneId, groupId));
+                if (isMuted)  {
+                    maxValue = minGain;
+                }
                 mCarAudioService.setGroupVolume(zoneId, groupId, maxValue, flags);
                 break;
             case AudioManager.ADJUST_MUTE:
@@ -93,7 +102,7 @@
                 setMute(adjustment == AudioManager.ADJUST_MUTE, groupId, flags);
                 break;
             case AudioManager.ADJUST_TOGGLE_MUTE:
-                toggleMute(groupId, flags);
+                setMute(!isMuted, groupId, flags);
                 break;
             case AudioManager.ADJUST_SAME:
             default:
@@ -101,13 +110,11 @@
         }
     }
 
-    private void toggleMute(int groupId, int flags) {
+    private boolean isMuted(int zoneId, int groupId) {
         if (mUseCarVolumeGroupMuting) {
-            setMute(!mCarAudioService.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId), groupId,
-                    flags);
-            return;
+            return mCarAudioService.isVolumeGroupMuted(zoneId, groupId);
         }
-        setMute(!mAudioManager.isMasterMute(), groupId, flags);
+        return mAudioManager.isMasterMute();
     }
 
     private void setMute(boolean mute, int groupId, int flags) {
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index 62be1fd..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) {
@@ -550,9 +532,9 @@
                 AudioManager.GET_DEVICES_INPUTS);
     }
 
+    @GuardedBy("mImplLock")
     private SparseArray<CarAudioZone> loadCarAudioConfigurationLocked(
-            List<CarAudioDeviceInfo> carAudioDeviceInfos) {
-        AudioDeviceInfo[] inputDevices = getAllInputDevices();
+            List<CarAudioDeviceInfo> carAudioDeviceInfos, AudioDeviceInfo[] inputDevices) {
         try (InputStream inputStream = new FileInputStream(mCarAudioConfigurationPath)) {
             CarAudioZonesHelper zonesHelper = new CarAudioZonesHelper(mCarAudioSettings,
                     inputStream, carAudioDeviceInfos, inputDevices, mUseCarVolumeGroupMuting);
@@ -564,8 +546,9 @@
         }
     }
 
+    @GuardedBy("mImplLock")
     private SparseArray<CarAudioZone> loadVolumeGroupConfigurationWithAudioControlLocked(
-            List<CarAudioDeviceInfo> carAudioDeviceInfos) {
+            List<CarAudioDeviceInfo> carAudioDeviceInfos, AudioDeviceInfo[] inputDevices) {
         AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
         if (!(audioControlWrapper instanceof AudioControlWrapperV1)) {
             throw new IllegalStateException(
@@ -574,20 +557,22 @@
         }
         CarAudioZonesHelperLegacy legacyHelper = new CarAudioZonesHelperLegacy(mContext,
                 R.xml.car_volume_groups, carAudioDeviceInfos,
-                (AudioControlWrapperV1) audioControlWrapper, mCarAudioSettings);
+                (AudioControlWrapperV1) audioControlWrapper, mCarAudioSettings, inputDevices);
         return legacyHelper.loadAudioZones();
     }
 
     @GuardedBy("mImplLock")
     private void loadCarAudioZonesLocked() {
         List<CarAudioDeviceInfo> carAudioDeviceInfos = generateCarAudioDeviceInfos();
+        AudioDeviceInfo[] inputDevices = getAllInputDevices();
 
         mCarAudioConfigurationPath = getAudioConfigurationPath();
         if (mCarAudioConfigurationPath != null) {
-            mCarAudioZones = loadCarAudioConfigurationLocked(carAudioDeviceInfos);
+            mCarAudioZones = loadCarAudioConfigurationLocked(carAudioDeviceInfos, inputDevices);
         } else {
-            mCarAudioZones = loadVolumeGroupConfigurationWithAudioControlLocked(
-                    carAudioDeviceInfos);
+            mCarAudioZones =
+                    loadVolumeGroupConfigurationWithAudioControlLocked(carAudioDeviceInfos,
+                            inputDevices);
         }
 
         CarAudioZonesValidator.validate(mCarAudioZones);
diff --git a/service/src/com/android/car/audio/CarAudioUtils.java b/service/src/com/android/car/audio/CarAudioUtils.java
index b787efb..ad74587 100644
--- a/service/src/com/android/car/audio/CarAudioUtils.java
+++ b/service/src/com/android/car/audio/CarAudioUtils.java
@@ -16,6 +16,10 @@
 
 package com.android.car.audio;
 
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+
+import android.media.AudioDeviceInfo;
+
 final class CarAudioUtils {
     private CarAudioUtils() {
     }
@@ -23,4 +27,8 @@
     static boolean hasExpired(long startTimeMs, long currentTimeMs, int timeoutMs) {
         return (currentTimeMs - startTimeMs) > timeoutMs;
     }
+
+    static boolean isMicrophoneInputDevice(AudioDeviceInfo device) {
+        return device.getType() == TYPE_BUILTIN_MIC;
+    }
 }
diff --git a/service/src/com/android/car/audio/CarAudioZonesHelper.java b/service/src/com/android/car/audio/CarAudioZonesHelper.java
index 7956ea2..2e25325 100644
--- a/service/src/com/android/car/audio/CarAudioZonesHelper.java
+++ b/service/src/com/android/car/audio/CarAudioZonesHelper.java
@@ -17,10 +17,14 @@
 
 import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
 
+import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
+
 import android.annotation.NonNull;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.Xml;
@@ -35,8 +39,6 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -73,7 +75,7 @@
     private static final Map<String, Integer> CONTEXT_NAME_MAP;
 
     static {
-        CONTEXT_NAME_MAP = new HashMap<>(CarAudioContext.CONTEXTS.length);
+        CONTEXT_NAME_MAP = new ArrayMap<>(CarAudioContext.CONTEXTS.length);
         CONTEXT_NAME_MAP.put("music", CarAudioContext.MUSIC);
         CONTEXT_NAME_MAP.put("navigation", CarAudioContext.NAVIGATION);
         CONTEXT_NAME_MAP.put("voice_command", CarAudioContext.VOICE_COMMAND);
@@ -128,11 +130,11 @@
 
     private final CarAudioSettings mCarAudioSettings;
     private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo;
-    private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfo;
+    private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfoForAllInputDevices;
     private final InputStream mInputStream;
     private final SparseIntArray mZoneIdToOccupantZoneIdMapping;
     private final Set<Integer> mAudioZoneIds;
-    private final Set<String> mInputAudioDevices;
+    private final Set<String> mAssignedInputAudioDevices;
     private final boolean mUseCarVolumeGroupMute;
 
     private int mNextSecondaryZoneId;
@@ -152,12 +154,12 @@
         Objects.requireNonNull(inputDeviceInfo);
         mAddressToCarAudioDeviceInfo = CarAudioZonesHelper.generateAddressToInfoMap(
                 carAudioDeviceInfos);
-        mAddressToInputAudioDeviceInfo =
+        mAddressToInputAudioDeviceInfoForAllInputDevices =
                 CarAudioZonesHelper.generateAddressToInputAudioDeviceInfoMap(inputDeviceInfo);
         mNextSecondaryZoneId = PRIMARY_AUDIO_ZONE + 1;
         mZoneIdToOccupantZoneIdMapping = new SparseIntArray();
-        mAudioZoneIds = new HashSet<>();
-        mInputAudioDevices = new HashSet<>();
+        mAudioZoneIds = new ArraySet<>();
+        mAssignedInputAudioDevices = new ArraySet<>();
         mUseCarVolumeGroupMute = useCarVolumeGroupMute;
     }
 
@@ -178,8 +180,8 @@
 
     private static Map<String, AudioDeviceInfo> generateAddressToInputAudioDeviceInfoMap(
             @NonNull AudioDeviceInfo[] inputAudioDeviceInfos) {
-        HashMap<String, AudioDeviceInfo> deviceAddressToInputDeviceMap =
-                new HashMap<>(inputAudioDeviceInfos.length);
+        Map<String, AudioDeviceInfo> deviceAddressToInputDeviceMap =
+                new ArrayMap<>(inputAudioDeviceInfos.length);
         for (int i = 0; i < inputAudioDeviceInfos.length; ++i) {
             AudioDeviceInfo device = inputAudioDeviceInfos[i];
             if (device.isSource()) {
@@ -238,9 +240,20 @@
         }
 
         verifyPrimaryZonePresent(carAudioZones);
+        addRemainingMicrophonesToPrimaryZone(carAudioZones);
         return carAudioZones;
     }
 
+    private void addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones) {
+        CarAudioZone primaryAudioZone = carAudioZones.get(PRIMARY_AUDIO_ZONE);
+        for (AudioDeviceInfo info : mAddressToInputAudioDeviceInfoForAllInputDevices.values()) {
+            if (!mAssignedInputAudioDevices.contains(info.getAddress())
+                    && isMicrophoneInputDevice(info)) {
+                primaryAudioZone.addInputAudioDevice(new AudioDeviceAttributes(info));
+            }
+        }
+    }
+
     private void verifyOnlyOnePrimaryZone(CarAudioZone newZone, SparseArray<CarAudioZone> zones) {
         if (newZone.getId() == PRIMARY_AUDIO_ZONE && zones.contains(PRIMARY_AUDIO_ZONE)) {
             throw new RuntimeException("More than one zone parsed with primary audio zone ID: "
@@ -347,7 +360,8 @@
                 String audioDeviceAddress =
                         parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
                 validateInputAudioDeviceAddress(audioDeviceAddress);
-                AudioDeviceInfo info = mAddressToInputAudioDeviceInfo.get(audioDeviceAddress);
+                AudioDeviceInfo info =
+                        mAddressToInputAudioDeviceInfoForAllInputDevices.get(audioDeviceAddress);
                 Preconditions.checkArgument(info != null,
                         "%s %s of %s does not exist, add input device to"
                                 + " audio_policy_configuration.xml.",
@@ -364,11 +378,11 @@
         Preconditions.checkArgument(!audioDeviceAddress.isEmpty(),
                 "%s %s attribute can not be empty.",
                 TAG_INPUT_DEVICE, ATTR_DEVICE_ADDRESS);
-        if (mInputAudioDevices.contains(audioDeviceAddress)) {
+        if (mAssignedInputAudioDevices.contains(audioDeviceAddress)) {
             throw new IllegalArgumentException(TAG_INPUT_DEVICE + " " + audioDeviceAddress
                     + " repeats, " + TAG_INPUT_DEVICES + " can not repeat.");
         }
-        mInputAudioDevices.add(audioDeviceAddress);
+        mAssignedInputAudioDevices.add(audioDeviceAddress);
     }
 
     private void validateOccupantZoneIdIsUnique(int occupantZoneId) {
diff --git a/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
index 6bbd6c9..7ce2538 100644
--- a/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
+++ b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
@@ -17,6 +17,7 @@
 
 import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
 
+import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
 import static com.android.car.audio.CarAudioZonesHelper.LEGACY_CONTEXTS;
 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEPRECATED_CODE;
 
@@ -25,6 +26,8 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
 import android.util.AttributeSet;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -62,15 +65,21 @@
     private final SparseIntArray mLegacyAudioContextToBus;
     private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo;
     private final CarAudioSettings mCarAudioSettings;
+    private final AudioDeviceInfo[] mInputDevices;
 
-    CarAudioZonesHelperLegacy(@NonNull  Context context, @XmlRes int xmlConfiguration,
+    CarAudioZonesHelperLegacy(@NonNull Context context, @XmlRes int xmlConfiguration,
             @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos,
             @NonNull AudioControlWrapperV1 audioControlWrapper,
-            @NonNull CarAudioSettings carAudioSettings) {
-        Objects.requireNonNull(context);
-        Objects.requireNonNull(carAudioDeviceInfos);
-        Objects.requireNonNull(audioControlWrapper);
-        mCarAudioSettings = Objects.requireNonNull(carAudioSettings);
+            @NonNull CarAudioSettings carAudioSettings,
+            AudioDeviceInfo[] inputDevices) {
+        Objects.requireNonNull(context, "Context must not be null.");
+        Objects.requireNonNull(carAudioDeviceInfos,
+                "Car Audio Device Info must not be null.");
+        Objects.requireNonNull(audioControlWrapper,
+                "Car Audio Control must not be null.");
+        Objects.requireNonNull(inputDevices, "Input Devices must not be null.");
+        mCarAudioSettings = Objects.requireNonNull(carAudioSettings,
+                "Car Audio Settings can not be null.");
         mContext = context;
         mXmlConfiguration = xmlConfiguration;
         mBusToCarAudioDeviceInfo =
@@ -78,6 +87,7 @@
 
         mLegacyAudioContextToBus =
                 loadBusesForLegacyContexts(audioControlWrapper);
+        mInputDevices = inputDevices;
     }
 
     /* Loads mapping from {@link CarAudioContext} values to bus numbers
@@ -135,7 +145,9 @@
             zone.addVolumeGroup(volumeGroup);
         }
         SparseArray<CarAudioZone> carAudioZones = new SparseArray<>();
+        addMicrophonesToPrimaryZone(zone);
         carAudioZones.put(PRIMARY_AUDIO_ZONE, zone);
+
         return carAudioZones;
     }
 
@@ -221,6 +233,15 @@
         return contexts;
     }
 
+    private void addMicrophonesToPrimaryZone(CarAudioZone primaryAudioZone) {
+        for (int index = 0; index < mInputDevices.length; index++) {
+            AudioDeviceInfo info = mInputDevices[index];
+            if (isMicrophoneInputDevice(info)) {
+                primaryAudioZone.addInputAudioDevice(new AudioDeviceAttributes(info));
+            }
+        }
+    }
+
     /**
      * Parse device address. Expected format is BUS%d_%s, address, usage hint
      *
diff --git a/service/src/com/android/car/audio/CarAudioZonesValidator.java b/service/src/com/android/car/audio/CarAudioZonesValidator.java
index 5fe495d..6b7bbb9 100644
--- a/service/src/com/android/car/audio/CarAudioZonesValidator.java
+++ b/service/src/com/android/car/audio/CarAudioZonesValidator.java
@@ -16,9 +16,16 @@
 package com.android.car.audio;
 
 
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+
+import android.media.AudioDeviceAttributes;
 import android.util.SparseArray;
 
+import com.android.internal.util.Preconditions;
+
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 final class CarAudioZonesValidator {
@@ -29,6 +36,20 @@
         validateAtLeastOneZoneDefined(carAudioZones);
         validateVolumeGroupsForEachZone(carAudioZones);
         validateEachAddressAppearsAtMostOnce(carAudioZones);
+        validatePrimaryZoneHasInputDevice(carAudioZones);
+    }
+
+    private static void validatePrimaryZoneHasInputDevice(SparseArray<CarAudioZone> carAudioZones) {
+        CarAudioZone primaryZone = carAudioZones.get(PRIMARY_AUDIO_ZONE);
+        List<AudioDeviceAttributes> devices = primaryZone.getInputAudioDevices();
+        Preconditions.checkCollectionNotEmpty(devices, "Primary Zone Input Devices");
+        for (int index = 0; index < devices.size(); index++) {
+            AudioDeviceAttributes device = devices.get(index);
+            if (device.getType() == TYPE_BUILTIN_MIC) {
+                return;
+            }
+        }
+        throw new RuntimeException("Primary Zone must have at least one microphone input device");
     }
 
     private static void validateAtLeastOneZoneDefined(SparseArray<CarAudioZone> carAudioZones) {
diff --git a/service/src/com/android/car/audio/CarDuckingUtils.java b/service/src/com/android/car/audio/CarDuckingUtils.java
index f386d62..e0998fc 100644
--- a/service/src/com/android/car/audio/CarDuckingUtils.java
+++ b/service/src/com/android/car/audio/CarDuckingUtils.java
@@ -126,6 +126,7 @@
 
     static List<String> getAddressesToDuck(int[] usages, CarAudioZone zone) {
         Set<Integer> uniqueContexts = CarAudioContext.getUniqueContextsForUsages(usages);
+        uniqueContexts.remove(INVALID);
         Set<Integer> contextsToDuck = getContextsToDuck(uniqueContexts);
         Set<String> addressesToDuck = getAddressesForContexts(contextsToDuck, zone);
 
diff --git a/service/src/com/android/car/audio/CarVolumeGroup.java b/service/src/com/android/car/audio/CarVolumeGroup.java
index 9c40725..8e98fea 100644
--- a/service/src/com/android/car/audio/CarVolumeGroup.java
+++ b/service/src/com/android/car/audio/CarVolumeGroup.java
@@ -150,30 +150,43 @@
 
     int getCurrentGainIndex() {
         synchronized (mLock) {
-            return mCurrentGainIndex;
+            if (mIsMuted) {
+                return getIndexForGain(mMinGain);
+            }
+            return getCurrentGainIndexLocked();
         }
     }
 
+    private int getCurrentGainIndexLocked() {
+        return mCurrentGainIndex;
+    }
+
     /**
      * Sets the gain on this group, gain will be set on all devices within volume group.
      */
     void setCurrentGainIndex(int gainIndex) {
-        int gainInMillibels = getGainForIndex(gainIndex);
         Preconditions.checkArgument(isValidGainIndex(gainIndex),
-                "Gain out of range (%d:%d) %d index %d", mMinGain, mMaxGain,
-                gainInMillibels, gainIndex);
+                "Gain out of range (%d:%d) index %d", mMinGain, mMaxGain, gainIndex);
         synchronized (mLock) {
-            for (String address : mAddressToCarAudioDeviceInfo.keySet()) {
-                CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
-                info.setCurrentGain(gainInMillibels);
+            if (mIsMuted) {
+                setMuteLocked(false);
             }
-
-            mCurrentGainIndex = gainIndex;
-
-            storeGainIndexForUserLocked(mCurrentGainIndex, mUserId);
+            setCurrentGainIndexLocked(gainIndex);
         }
     }
 
+    private void setCurrentGainIndexLocked(int gainIndex) {
+        int gainInMillibels = getGainForIndex(gainIndex);
+        for (String address : mAddressToCarAudioDeviceInfo.keySet()) {
+            CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
+            info.setCurrentGain(gainInMillibels);
+        }
+
+        mCurrentGainIndex = gainIndex;
+
+        storeGainIndexForUserLocked(mCurrentGainIndex, mUserId);
+    }
+
     @Nullable
     AudioDevicePort getAudioDevicePortForContext(int carAudioContext) {
         final String address = mContextToAddress.get(carAudioContext);
@@ -233,18 +246,22 @@
             updateUserIdLocked(userId);
             //Update the current gain index
             updateCurrentGainIndexLocked();
+            setCurrentGainIndexLocked(getCurrentGainIndexLocked());
             //Reset devices with current gain index
             updateGroupMuteLocked();
         }
-        setCurrentGainIndex(getCurrentGainIndex());
     }
 
     void setMute(boolean mute) {
         synchronized (mLock) {
-            mIsMuted = mute;
-            if (mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) {
-                mSettingsManager.storeVolumeGroupMuteForUser(mUserId, mZoneId, mId, mute);
-            }
+            setMuteLocked(mute);
+        }
+    }
+
+    void setMuteLocked(boolean mute) {
+        mIsMuted = mute;
+        if (mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) {
+            mSettingsManager.storeVolumeGroupMuteForUser(mUserId, mZoneId, mId, mute);
         }
     }
 
diff --git a/service/src/com/android/car/evs/CarEvsService.java b/service/src/com/android/car/evs/CarEvsService.java
index 1b19249..5c1a8ab 100644
--- a/service/src/com/android/car/evs/CarEvsService.java
+++ b/service/src/com/android/car/evs/CarEvsService.java
@@ -200,6 +200,9 @@
             synchronized (mLock) {
                 if (requestActivityIfNecessaryLocked()) {
                     Slog.i(TAG_EVS, "Requested to launch the activity.");
+                } else {
+                    // Ensure we stops streaming
+                    mStateEngine.execute(REQUEST_PRIORITY_HIGH, SERVICE_STATE_INACTIVE);
                 }
             }
         }
@@ -591,6 +594,9 @@
                 return;
             }
 
+            // Notify the client that the stream has ended.
+            notifyStreamStopped(callback);
+
             unlinkToDeathStreamCallbackLocked();
             mStreamCallback = null;
             Slog.i(TAG_EVS, "Last stream client has been disconnected.");
@@ -625,6 +631,8 @@
             return false;
         }
 
+        // Request to launch an activity again after cleaning up
+        mStateEngine.execute(REQUEST_PRIORITY_HIGH, SERVICE_STATE_INACTIVE);
         mStateEngine.execute(REQUEST_PRIORITY_HIGH, SERVICE_STATE_REQUESTED,
                 mLastEvsHalEvent.getServiceType());
         return true;
@@ -708,8 +716,8 @@
 
         synchronized (mLock) {
             int targetState = on ? SERVICE_STATE_REQUESTED : SERVICE_STATE_INACTIVE;
-            if (mStateEngine.execute(REQUEST_PRIORITY_HIGH, targetState, type) !=
-                    ERROR_NONE) {
+            if (mStateEngine.execute(REQUEST_PRIORITY_HIGH, targetState, type, /* token = */ null,
+                    mStreamCallback) != ERROR_NONE) {
                 Slog.e(TAG_EVS, "Failed to execute a service request.");
             }
 
@@ -1234,23 +1242,30 @@
         }
     }
 
-    /** Processes a streaming event and propagates it to registered clients */
-    private void processNewFrame(int id, @NonNull HardwareBuffer buffer) {
+    /**
+     * Processes a streaming event and propagates it to registered clients.
+     *
+     * @return True if this buffer is hold and used by the client, false otherwise.
+     */
+    private boolean processNewFrame(int id, @NonNull HardwareBuffer buffer) {
         Objects.requireNonNull(buffer);
 
         synchronized (mLock) {
-            mBufferRecords.add(id);
             if (mStreamCallback == null) {
-                return;
+                return false;
             }
 
             try {
                 mStreamCallback.onNewFrame(new CarEvsBufferDescriptor(id, buffer));
+                mBufferRecords.add(id);
             } catch (RemoteException e) {
                 // Likely the binder death incident
                 Slog.e(TAG_EVS, Log.getStackTraceString(e));
+                return false;
             }
         }
+
+        return true;
     }
 
     /** EVS stream event handler called after a native handler */
@@ -1261,7 +1276,11 @@
 
     /** EVS frame handler called after a native handler */
     private void postNativeFrameHandler(int id, HardwareBuffer buffer) {
-        processNewFrame(id, buffer);
+        if (!processNewFrame(id, buffer)) {
+            // No client uses this buffer.
+            Slog.d(TAG_EVS, "Returns buffer " + id + " because no client uses it.");
+            nativeDoneWithFrame(mNativeEvsServiceObj, id);
+        }
     }
 
     /** EVS service death handler called after a native handler */
diff --git a/service/src/com/android/car/garagemode/Controller.java b/service/src/com/android/car/garagemode/Controller.java
index 81c8c10..fdf0c05 100644
--- a/service/src/com/android/car/garagemode/Controller.java
+++ b/service/src/com/android/car/garagemode/Controller.java
@@ -16,6 +16,8 @@
 
 package com.android.car.garagemode;
 
+import static com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
+
 import android.app.job.JobScheduler;
 import android.car.hardware.power.CarPowerManager;
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
@@ -28,6 +30,7 @@
 
 import com.android.car.CarLocalServices;
 import com.android.car.CarLog;
+import com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport;
 import com.android.car.systeminterface.SystemInterface;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.utils.Slogf;
@@ -116,6 +119,7 @@
     /**
      * Prints Garage Mode's status, including what jobs it is waiting for
      */
+    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
     void dump(PrintWriter writer) {
         mGarageMode.dump(writer);
     }
diff --git a/service/src/com/android/car/garagemode/GarageMode.java b/service/src/com/android/car/garagemode/GarageMode.java
index f315dca..5936391 100644
--- a/service/src/com/android/car/garagemode/GarageMode.java
+++ b/service/src/com/android/car/garagemode/GarageMode.java
@@ -16,6 +16,8 @@
 
 package com.android.car.garagemode;
 
+import static com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
+
 import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
 import android.app.job.JobSnapshot;
@@ -30,6 +32,7 @@
 import com.android.car.CarLocalServices;
 import com.android.car.CarLog;
 import com.android.car.CarStatsLogHelper;
+import com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport;
 import com.android.car.power.CarPowerManagementService;
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
@@ -242,6 +245,7 @@
         }
     }
 
+    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
     void dump(PrintWriter writer) {
         if (!mGarageModeActive) {
             return;
diff --git a/service/src/com/android/car/hal/CarPropertyUtils.java b/service/src/com/android/car/hal/CarPropertyUtils.java
index 9ac2745..146a4fb 100644
--- a/service/src/com/android/car/hal/CarPropertyUtils.java
+++ b/service/src/com/android/car/hal/CarPropertyUtils.java
@@ -260,6 +260,12 @@
         int areaType = getVehicleAreaType(p.prop & VehicleArea.MASK);
 
         Class<?> clazz = getJavaClass(p.prop & VehiclePropertyType.MASK);
+        float maxSampleRate = 0f;
+        float minSampleRate = 0f;
+        if (p.changeMode != CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_STATIC) {
+            maxSampleRate = p.maxSampleRate;
+            minSampleRate = p.minSampleRate;
+        }
         if (p.areaConfigs.isEmpty()) {
             return CarPropertyConfig
                     .newBuilder(clazz, propertyId, areaType, /* capacity */ 1)
@@ -268,8 +274,8 @@
                     .setChangeMode(p.changeMode)
                     .setConfigArray(p.configArray)
                     .setConfigString(p.configString)
-                    .setMaxSampleRate(p.maxSampleRate)
-                    .setMinSampleRate(p.minSampleRate)
+                    .setMaxSampleRate(maxSampleRate)
+                    .setMinSampleRate(minSampleRate)
                     .build();
         } else {
             CarPropertyConfig.Builder builder = CarPropertyConfig
@@ -278,8 +284,8 @@
                     .setChangeMode(p.changeMode)
                     .setConfigArray(p.configArray)
                     .setConfigString(p.configString)
-                    .setMaxSampleRate(p.maxSampleRate)
-                    .setMinSampleRate(p.minSampleRate);
+                    .setMaxSampleRate(maxSampleRate)
+                    .setMinSampleRate(minSampleRate);
 
             for (VehicleAreaConfig area : p.areaConfigs) {
                 if (classMatched(Integer.class, clazz)) {
diff --git a/service/src/com/android/car/hal/VehicleHal.java b/service/src/com/android/car/hal/VehicleHal.java
index 5de5a81..726f34f 100644
--- a/service/src/com/android/car/hal/VehicleHal.java
+++ b/service/src/com/android/car/hal/VehicleHal.java
@@ -39,6 +39,7 @@
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyChangeMode;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyType;
+import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
@@ -86,8 +87,8 @@
     public static final int NO_AREA = -1;
     public static final float NO_SAMPLE_RATE = -1;
 
-    private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
-            VehicleHal.class.getSimpleName());
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
     private final PowerHalService mPowerHal;
     private final PropertyHalService mPropertyHal;
     private final InputHalService mInputHal;
@@ -124,6 +125,9 @@
      * both passed as parameters.
      */
     public VehicleHal(Context context, IVehicle vehicle) {
+        mHandlerThread = CarServiceUtils.getHandlerThread(
+                VehicleHal.class.getSimpleName());
+        mHandler = new Handler(mHandlerThread.getLooper());
         mPowerHal = new PowerHalService(this);
         mPropertyHal = new PropertyHalService(this);
         mInputHal = new InputHalService(this);
@@ -156,7 +160,10 @@
             UserHalService userHal,
             DiagnosticHalService diagnosticHal,
             ClusterHalService clusterHalService,
-            HalClient halClient) {
+            HalClient halClient,
+            HandlerThread handlerThread) {
+        mHandlerThread = handlerThread;
+        mHandler = new Handler(mHandlerThread.getLooper());
         mPowerHal = powerHal;
         mPropertyHal = propertyHal;
         mInputHal = inputHal;
@@ -581,6 +588,7 @@
 
     private final ArraySet<HalServiceBase> mServicesToDispatch = new ArraySet<>();
 
+    // should be posted to the mHandlerThread
     @Override
     public void onPropertyEvent(ArrayList<VehiclePropValue> propValues) {
         synchronized (mLock) {
@@ -614,6 +622,7 @@
         // No need to handle on-property-set events in HAL service yet.
     }
 
+    // should be posted to the mHandlerThread
     @Override
     public void onPropertySetError(@CarPropertyManager.CarSetPropertyErrorCode int errorCode,
             int propId, int areaId) {
@@ -800,7 +809,7 @@
         }
         // update timestamp
         v.timestamp = SystemClock.elapsedRealtimeNanos() + TimeUnit.SECONDS.toNanos(delayTime);
-        onPropertyEvent(Lists.newArrayList(v));
+        mHandler.post(() -> onPropertyEvent(Lists.newArrayList(v)));
     }
 
     /**
@@ -838,7 +847,7 @@
                     // Avoid the fake events be covered by real Event
                     v.timestamp = SystemClock.elapsedRealtimeNanos()
                             + TimeUnit.SECONDS.toNanos(timeDurationInSec);
-                    onPropertyEvent(Lists.newArrayList(v));
+                    mHandler.post(() -> onPropertyEvent(Lists.newArrayList(v)));
                 }
             }
         }, /* delay= */0, period);
diff --git a/service/src/com/android/car/pm/CarPackageManagerService.java b/service/src/com/android/car/pm/CarPackageManagerService.java
index 4c262b5..e8e4315 100644
--- a/service/src/com/android/car/pm/CarPackageManagerService.java
+++ b/service/src/com/android/car/pm/CarPackageManagerService.java
@@ -55,12 +55,17 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.LocalLog;
@@ -83,9 +88,13 @@
 import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
 
 import com.google.android.collect.Sets;
 
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -108,6 +117,7 @@
     private static final String PACKAGE_DELIMITER = ",";
     private static final String PACKAGE_ACTIVITY_DELIMITER = "/";
     private static final int LOG_SIZE = 20;
+    private static final String[] WINDOW_DUMP_ARGUMENTS = new String[]{"windows"};
 
     private static final String PROPERTY_RO_DRIVING_SAFETY_REGION =
             "ro.android.car.drivingsafetyregion";
@@ -117,6 +127,7 @@
     private final PackageManager mPackageManager;
     private final ActivityManager mActivityManager;
     private final DisplayManager mDisplayManager;
+    private final IBinder mWindowManagerBinder;
 
     private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
             getClass().getSimpleName());
@@ -137,6 +148,9 @@
 
     private final List<String> mAllowedAppInstallSources;
 
+    @GuardedBy("mLock")
+    private final SparseArray<ComponentName> mTopActivityWithDialogPerDisplay = new SparseArray<>();
+
     /**
      * Hold policy set from policy service or client.
      * Key: packageName of policy service
@@ -164,6 +178,8 @@
     private final boolean mEnableActivityBlocking;
 
     private final ComponentName mActivityBlockingActivity;
+    private final boolean mPreventTemplatedAppsFromShowingDialog;
+    private final String mTemplateActivityClassName;
 
     private final ActivityLaunchListener mActivityLaunchListener = new ActivityLaunchListener();
 
@@ -252,6 +268,7 @@
         mPackageManager = mContext.getPackageManager();
         mActivityManager = mContext.getSystemService(ActivityManager.class);
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
+        mWindowManagerBinder = ServiceManager.getService(Context.WINDOW_SERVICE);
         Resources res = context.getResources();
         mEnableActivityBlocking = res.getBoolean(R.bool.enableActivityBlockingForSafety);
         String blockingActivity = res.getString(R.string.activityBlockingActivity);
@@ -260,6 +277,9 @@
                 res.getStringArray(R.array.allowedAppInstallSources));
         mVendorServiceController = new VendorServiceController(
                 mContext, mHandler.getLooper());
+        mPreventTemplatedAppsFromShowingDialog =
+                res.getBoolean(R.bool.config_preventTemplatedAppsFromShowingDialog);
+        mTemplateActivityClassName = res.getString(R.string.config_template_activity_class_name);
     }
 
 
@@ -414,6 +434,15 @@
                 return true;
             }
 
+            for (int i = mTopActivityWithDialogPerDisplay.size() - 1; i >= 0; i--) {
+                ComponentName activityWithDialog = mTopActivityWithDialogPerDisplay.get(
+                        mTopActivityWithDialogPerDisplay.keyAt(i));
+                if (activityWithDialog.getClassName().equals(className)
+                        && activityWithDialog.getPackageName().equals(packageName)) {
+                    return false;
+                }
+            }
+
             if (searchFromClientPolicyBlocklistsLocked(packageName)) {
                 return false;
             }
@@ -1166,6 +1195,9 @@
         synchronized (mLock) {
             writer.println("*CarPackageManagerService*");
             writer.println("mEnableActivityBlocking:" + mEnableActivityBlocking);
+            writer.println("mPreventTemplatedAppsFromShowingDialog:"
+                    + mPreventTemplatedAppsFromShowingDialog);
+            writer.println("mTemplateActivityClassName:" + mTemplateActivityClassName);
             List<String> restrictions = new ArrayList<>(mUxRestrictionsListeners.size());
             for (int i = 0; i < mUxRestrictionsListeners.size(); i++) {
                 int displayId = mUxRestrictionsListeners.keyAt(i);
@@ -1275,6 +1307,14 @@
     }
 
     private void blockTopActivityIfNecessary(TopTaskInfoContainer topTask) {
+        synchronized (mLock) {
+            if (mTopActivityWithDialogPerDisplay.contains(topTask.displayId)
+                    && !topTask.topActivity.equals(
+                            mTopActivityWithDialogPerDisplay.get(topTask.displayId))) {
+                // Clear top activity-with-dialog if the activity has changed on this display.
+                mTopActivityWithDialogPerDisplay.remove(topTask.displayId);
+            }
+        }
         if (isUxRestrictedOnDisplay(topTask.displayId)) {
             doBlockTopActivityIfNotAllowed(topTask);
         }
@@ -1284,10 +1324,7 @@
         if (topTask.topActivity == null) {
             return;
         }
-
-        boolean allowed = isActivityDistractionOptimized(
-                topTask.topActivity.getPackageName(),
-                topTask.topActivity.getClassName());
+        boolean allowed = isActivityAllowed(topTask);
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Slog.d(TAG, "new activity:" + topTask.toString() + " allowed:" + allowed);
         }
@@ -1320,9 +1357,10 @@
 
         boolean isRootDO = false;
         if (taskRootActivity != null) {
-            ComponentName componentName = ComponentName.unflattenFromString(taskRootActivity);
+            ComponentName taskRootComponentName =
+                    ComponentName.unflattenFromString(taskRootActivity);
             isRootDO = isActivityDistractionOptimized(
-                    componentName.getPackageName(), componentName.getClassName());
+                    taskRootComponentName.getPackageName(), taskRootComponentName.getClassName());
         }
 
         Intent newActivityIntent = createBlockingActivityIntent(
@@ -1339,6 +1377,82 @@
         mSystemActivityMonitoringService.blockActivity(topTask, newActivityIntent);
     }
 
+    private boolean isActivityAllowed(TopTaskInfoContainer topTaskInfoContainer) {
+        ComponentName activityName = topTaskInfoContainer.topActivity;
+        boolean isDistractionOptimized = isActivityDistractionOptimized(
+                activityName.getPackageName(),
+                activityName.getClassName());
+        if (!isDistractionOptimized) {
+            return false;
+        }
+        return !(mPreventTemplatedAppsFromShowingDialog
+                && isTemplateActivity(activityName)
+                && isActivityShowingADialogOnDisplay(activityName, topTaskInfoContainer.displayId));
+    }
+
+    private boolean isTemplateActivity(ComponentName activityName) {
+        // TODO(b/191263486): Finalise on how to detect the templated activities.
+        return activityName.getClassName().equals(mTemplateActivityClassName);
+    }
+
+    private boolean isActivityShowingADialogOnDisplay(ComponentName activityName, int displayId) {
+        String output = dumpWindows();
+        List<WindowDumpParser.Window> appWindows =
+                WindowDumpParser.getParsedAppWindows(output, activityName.getPackageName());
+        // TODO(b/192354699): Handle case where an activity can have multiple instances on the same
+        //  display.
+        int totalAppWindows = appWindows.size();
+        String firstActivityRecord = null;
+        int numTopActivityAppWindowsOnDisplay = 0;
+        for (int i = 0; i < totalAppWindows; i++) {
+            WindowDumpParser.Window appWindow = appWindows.get(i);
+            if (appWindow.getDisplayId() != displayId) {
+                continue;
+            }
+            if (TextUtils.isEmpty(appWindow.getActivityRecord())) {
+                continue;
+            }
+            if (firstActivityRecord == null) {
+                firstActivityRecord = appWindow.getActivityRecord();
+            }
+            if (firstActivityRecord.equals(appWindow.getActivityRecord())) {
+                numTopActivityAppWindowsOnDisplay++;
+            }
+        }
+        Slogf.d(TAG, "Top activity =  " + activityName);
+        Slogf.d(TAG, "Number of app widows of top activity = " + numTopActivityAppWindowsOnDisplay);
+        boolean isShowingADialog = numTopActivityAppWindowsOnDisplay > 1;
+        synchronized (mLock) {
+            if (isShowingADialog) {
+                mTopActivityWithDialogPerDisplay.put(displayId, activityName);
+            } else {
+                mTopActivityWithDialogPerDisplay.remove(displayId);
+            }
+        }
+        return isShowingADialog;
+    }
+
+    private String dumpWindows() {
+        try {
+            ParcelFileDescriptor[] fileDescriptors = ParcelFileDescriptor.createSocketPair();
+            mWindowManagerBinder.dump(
+                    fileDescriptors[0].getFileDescriptor(), WINDOW_DUMP_ARGUMENTS);
+            fileDescriptors[0].close();
+            StringBuilder outputBuilder = new StringBuilder();
+            BufferedReader reader = new BufferedReader(
+                    new FileReader(fileDescriptors[1].getFileDescriptor()));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                outputBuilder.append(line).append("\n");
+            }
+            reader.close();
+            fileDescriptors[1].close();
+            return outputBuilder.toString();
+        } catch (IOException | RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     /**
      * Creates an intent to start blocking activity.
      *
@@ -1346,6 +1460,7 @@
      * @param blockedActivity  the activity being blocked
      * @param blockedTaskId    the blocked task id, which contains the blocked activity
      * @param taskRootActivity root activity of the blocked task
+     * @param isRootDo         denotes if the root activity is distraction optimised
      * @return an intent to launch the blocking activity.
      */
     private static Intent createBlockingActivityIntent(ComponentName blockingActivity,
@@ -1644,6 +1759,15 @@
     }
 
     /**
+     * Called when a window change event is received by the {@link CarSafetyAccessibilityService}.
+     */
+    @VisibleForTesting
+    void onWindowChangeEvent() {
+        Slogf.d(TAG, "onWindowChange event received");
+        mHandlerThread.getThreadHandler().post(() -> blockTopActivitiesIfNecessary());
+    }
+
+    /**
      * Listens to the package install/uninstall events to know when to initiate parsing
      * installed packages.
      */
diff --git a/service/src/com/android/car/pm/CarSafetyAccessibilityService.java b/service/src/com/android/car/pm/CarSafetyAccessibilityService.java
new file mode 100644
index 0000000..76a6246
--- /dev/null
+++ b/service/src/com/android/car/pm/CarSafetyAccessibilityService.java
@@ -0,0 +1,41 @@
+/*
+ * 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.pm;
+
+import android.accessibilityservice.AccessibilityService;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.car.CarLocalServices;
+
+/**
+ * An accessibility service to notify the Car Service of any change in the window state. The car
+ * safety related code can read the events sent from this service and take the necessary actions.
+ */
+public class CarSafetyAccessibilityService extends AccessibilityService {
+    @Override
+    public void onAccessibilityEvent(AccessibilityEvent event) {
+        CarPackageManagerService carPackageManagerService =
+                CarLocalServices.getService(CarPackageManagerService.class);
+        if (carPackageManagerService != null) {
+            carPackageManagerService.onWindowChangeEvent();
+        }
+    }
+
+    @Override
+    public void onInterrupt() {
+    }
+}
diff --git a/service/src/com/android/car/pm/TEST_MAPPING b/service/src/com/android/car/pm/TEST_MAPPING
index d37fe3d..1c32824 100644
--- a/service/src/com/android/car/pm/TEST_MAPPING
+++ b/service/src/com/android/car/pm/TEST_MAPPING
@@ -19,6 +19,12 @@
         },
         {
           "include-filter": "com.android.car.pm.VendorServiceInfoTest"
+        },
+        {
+          "include-filter": "com.android.car.pm.CarSafetyAccessibilityServiceTest"
+        },
+        {
+          "include-filter": "com.android.car.pm.WindowDumpParserTest"
         }
       ]
     },
diff --git a/service/src/com/android/car/pm/WindowDumpParser.java b/service/src/com/android/car/pm/WindowDumpParser.java
new file mode 100644
index 0000000..8387ce7
--- /dev/null
+++ b/service/src/com/android/car/pm/WindowDumpParser.java
@@ -0,0 +1,124 @@
+/*
+ * 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.pm;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A utility class to parse the window dump.
+ */
+class WindowDumpParser {
+    private static final String WINDOW_TYPE_APPLICATION_STARTING = "APPLICATION_STARTING";
+
+    /**
+     * Parses the provided window dump and returns the list of windows only for a particular app.
+     *
+     * @param dump the window dump in string format as returned from `dumpsys window
+     *         windows`.
+     * @param appPackageName the package name of the app, which the windows are being
+     *         requested for.
+     * @return a list of parsed {@link Window} objects.
+     */
+    public static List<Window> getParsedAppWindows(String dump, String appPackageName) {
+        Pattern dumpSplitter = Pattern.compile("(Window #)|\\n\\n");
+        // \\n\\n to separate out the Global dump from the windows list.
+
+        Pattern windowDetailsPattern = Pattern.compile("\\d*.*\\n"
+                        + ".*mDisplayId=(\\S*).*\\n"
+                        + ".*package=(\\S*).*\\n"
+                        + ".*ty=(\\S*)"
+                        + "((.*ActivityRecord\\{(.*?)\\}.*\\n)|(.*\\n))*"
+                // (.*\\n) is required for skipping the lines before the line containing
+                // ActivityRecord{}.
+        );
+        List<Window> windows = new ArrayList<>();
+
+        String[] windowDumps = dumpSplitter.split(dump);
+        for (int i = 1; i < windowDumps.length - 1; i++) {
+            Matcher m = windowDetailsPattern.matcher(windowDumps[i]);
+            if (m.find()) {
+                // Only consider windows for the given appPackageName which are not the splash
+                // screen windows.
+                // TODO(b/192355798): Revisit this logic as window type can be changed.
+                if (Objects.equals(m.group(2), appPackageName)
+                        && !Objects.equals(m.group(3), WINDOW_TYPE_APPLICATION_STARTING)) {
+                    windows.add(new Window(
+                            /* packageName = */ m.group(2),
+                            /* displayId = */ Integer.parseInt(m.group(1)),
+                            /* activityRecord = */ m.group(6)
+                    ));
+                }
+            }
+        }
+        return windows;
+    }
+
+    /**
+     * A holder class that represents an app's window.
+     */
+    static class Window {
+        private final String mPackageName;
+        private final int mDisplayId;
+        private final String mActivityRecord;
+
+        Window(String packageName, int displayId, String activityRecord) {
+            mPackageName = packageName;
+            mDisplayId = displayId;
+            mActivityRecord = activityRecord;
+        }
+
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        public int getDisplayId() {
+            return mDisplayId;
+        }
+
+        public String getActivityRecord() {
+            return mActivityRecord;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof Window)) return false;
+            Window window = (Window) o;
+            return mDisplayId == window.mDisplayId
+                    && mPackageName.equals(window.mPackageName)
+                    && Objects.equals(mActivityRecord, window.mActivityRecord);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPackageName, mDisplayId, mActivityRecord);
+        }
+
+        @Override
+        public String toString() {
+            return "Window{"
+                    + "mPackageName=" + mPackageName
+                    + ", mDisplayId=" + mDisplayId
+                    + ", mActivityRecord={" + mActivityRecord + "}"
+                    + "}";
+        }
+    }
+}
diff --git a/service/src/com/android/car/power/PowerComponentHandler.java b/service/src/com/android/car/power/PowerComponentHandler.java
index dcf22e8..f6f1b3b 100644
--- a/service/src/com/android/car/power/PowerComponentHandler.java
+++ b/service/src/com/android/car/power/PowerComponentHandler.java
@@ -113,13 +113,8 @@
                 mComponentStates.put(component, false);
                 PowerComponentMediator mediator = factory.createPowerComponent(component);
                 String componentName = powerComponentToString(component);
-                if (mediator == null) {
-                    Slogf.w(TAG, "Power component(%s) is not valid or doesn't need a mediator",
-                            componentName);
-                    continue;
-                }
-                if (!mediator.isComponentAvailable()) {
-                    Slogf.w(TAG, "Power component(%s) is not available", componentName);
+                if (mediator == null || !mediator.isComponentAvailable()) {
+                    // We don't not associate a mediator with the component.
                     continue;
                 }
                 mPowerComponentMediators.put(component, mediator);
@@ -237,7 +232,6 @@
 
         PowerComponentMediator mediator = mPowerComponentMediators.get(component);
         if (mediator == null) {
-            Slogf.w(TAG, "%s doesn't have a mediator", powerComponentToString(component));
             return true;
         }
 
diff --git a/service/src/com/android/car/telemetry/CarTelemetryService.java b/service/src/com/android/car/telemetry/CarTelemetryService.java
index 695e25e..f48d8d8 100644
--- a/service/src/com/android/car/telemetry/CarTelemetryService.java
+++ b/service/src/com/android/car/telemetry/CarTelemetryService.java
@@ -15,28 +15,44 @@
  */
 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_NONE;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_PARSE_FAILED;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_UNKNOWN;
 
 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.os.Handler;
+import android.os.HandlerThread;
+import android.os.PersistableBundle;
+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.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
 
 /**
  * CarTelemetryService manages OEM telemetry collection, processing and communication
@@ -44,32 +60,55 @@
  */
 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 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 StatsManagerProxy mStatsManagerProxy;
+    private SystemMonitor mSystemMonitor;
 
-    public CarTelemetryService(Context context) {
+    public CarTelemetryService(Context context, CarPropertyService carPropertyService) {
         mContext = context;
+        mCarPropertyService = carPropertyService;
     }
 
     @Override
     public void init() {
-        // nothing to do
+        mTelemetryHandler.post(() -> {
+            SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class);
+            // full root directory path is /data/system/car/telemetry
+            File rootDirectory = new File(systemInterface.getSystemCarDir(), TELEMETRY_DIR);
+            // initialize all necessary components
+            mMetricsConfigStore = new MetricsConfigStore(rootDirectory);
+            mResultStore = new ResultStore(rootDirectory);
+            mStatsManagerProxy = new StatsManagerImpl(
+                    mContext.getSystemService(StatsManager.class));
+            mPublisherFactory = new PublisherFactory(mCarPropertyService, mTelemetryHandler,
+                    mStatsManagerProxy, rootDirectory);
+            mDataBroker = new DataBrokerImpl(mContext, mPublisherFactory, mResultStore);
+            mSystemMonitor = SystemMonitor.create(mContext, mTelemetryHandler);
+            // controller starts metrics collection after boot complete
+            mDataBrokerController = new DataBrokerController(mDataBroker, mTelemetryHandler,
+                    mMetricsConfigStore, mSystemMonitor,
+                    systemInterface.getSystemStateInterface());
+        });
     }
 
     @Override
     public void release() {
-        // nothing to do
+        // TODO(b/197969149): prevent threading issue, block main thread
+        mTelemetryHandler.post(() -> mResultStore.flushToDisk());
     }
 
     @Override
@@ -85,9 +124,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,150 +138,181 @@
     @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. This method assumes
+     * {@link #setListener(ICarTelemetryServiceListener)} is called. Otherwise it does nothing.
      *
-     * @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");
+        mTelemetryHandler.post(() -> {
+            if (mListener == null) {
+                Slog.w(CarLog.TAG_TELEMETRY, "ICarTelemetryServiceListener is not set");
+                return;
+            }
+            Slog.d(CarLog.TAG_TELEMETRY, "Adding metrics config " + key.getName()
+                    + " to car telemetry service");
+            TelemetryProto.MetricsConfig metricsConfig = null;
+            int status = ERROR_METRICS_CONFIG_UNKNOWN;
+            try {
+                metricsConfig = TelemetryProto.MetricsConfig.parseFrom(config);
+            } catch (InvalidProtocolBufferException e) {
+                Slog.e(CarLog.TAG_TELEMETRY, "Failed to parse MetricsConfig.", e);
+                status = ERROR_METRICS_CONFIG_PARSE_FAILED;
+            }
+            // if config can be parsed, add it to persistent storage
+            if (metricsConfig != null) {
+                status = mMetricsConfigStore.addMetricsConfig(metricsConfig);
+                // TODO(b/199410900): update logic once metrics configs have expiration dates
+                mDataBroker.addMetricsConfiguration(metricsConfig);
+            }
+            // If no error (a config is successfully added), script results from an older version
+            // should be deleted
+            if (status == ERROR_METRICS_CONFIG_NONE) {
+                mResultStore.deleteResult(key.getName());
+            }
+            try {
+                mListener.onAddMetricsConfigStatus(key, status);
+            } catch (RemoteException e) {
+                Slog.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
+            }
+        });
     }
 
     /**
-     * Removes a manifest based on the key.
+     * Removes a metrics config based on the key. This will also remove outputs produced by the
+     * MetricsConfig. This method assumes {@link #setListener(ICarTelemetryServiceListener)} is
+     * called. Otherwise it does nothing.
+     *
+     * @param key the unique identifier of a MetricsConfig.
      */
     @Override
-    public boolean removeManifest(@NonNull ManifestKey key) {
-        mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
-        synchronized (mLock) {
-            return removeManifestLocked(key);
-        }
+    public void removeMetricsConfig(@NonNull MetricsConfigKey key) {
+        mTelemetryHandler.post(() -> {
+            if (mListener == null) {
+                Slog.w(CarLog.TAG_TELEMETRY, "ICarTelemetryServiceListener is not set");
+                return;
+            }
+            Slog.d(CarLog.TAG_TELEMETRY, "Removing metrics config " + key.getName()
+                    + " from car telemetry service");
+            // TODO(b/198792767): Check both config name and config version for deletion
+            // TODO(b/199540952): Stop and remove config from data broker
+            mResultStore.deleteResult(key.getName()); // delete the config's script results
+            boolean success = mMetricsConfigStore.deleteMetricsConfig(key.getName());
+            try {
+                mListener.onRemoveMetricsConfigStatus(key, success);
+            } catch (RemoteException e) {
+                Slog.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
+            }
+        });
     }
 
     /**
-     * Removes all manifests.
+     * Removes all MetricsConfigs. This will also remove all MetricsConfig outputs.
      */
     @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(() -> {
+            // TODO(b/199540952): Stop and remove all configs from DataBroker
+            Slog.d(CarLog.TAG_TELEMETRY,
+                    "Removing all metrics config from car telemetry service");
+            mMetricsConfigStore.deleteAllMetricsConfigs();
+            mResultStore.deleteAllResults();
+        });
     }
 
     /**
      * Sends script results associated with the given key using the
-     * {@link ICarTelemetryServiceListener}.
+     * {@link ICarTelemetryServiceListener}. This method assumes listener is set. Otherwise it
+     * does nothing.
+     *
+     * @param key the unique identifier of a MetricsConfig.
      */
     @Override
-    public void sendFinishedReports(@NonNull ManifestKey key) {
-        // TODO(b/184087869): Implement
+    public void sendFinishedReports(@NonNull MetricsConfigKey key) {
         mContext.enforceCallingOrSelfPermission(
-                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
-        if (DEBUG) {
-            Slog.d(TAG, "Flushing reports for a manifest");
-        }
+                Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "sendFinishedReports");
+        mTelemetryHandler.post(() -> {
+            if (mListener == null) {
+                Slog.w(CarLog.TAG_TELEMETRY, "ICarTelemetryServiceListener is not set");
+                return;
+            }
+            if (DEBUG) {
+                Slog.d(CarLog.TAG_TELEMETRY,
+                        "Flushing reports for metrics config " + key.getName());
+            }
+            PersistableBundle result = mResultStore.getFinalResult(key.getName(), true);
+            TelemetryProto.TelemetryError error = mResultStore.getError(key.getName(), true);
+            if (result != null) {
+                sendFinalResult(key, result);
+            } else if (error != null) {
+                sendError(key, error);
+            } else {
+                Slog.w(CarLog.TAG_TELEMETRY, "config " + key.getName()
+                        + " did not produce any results");
+            }
+        });
     }
 
     /**
-     * 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");
+    private void sendFinalResult(MetricsConfigKey key, PersistableBundle result) {
+        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+            result.writeToStream(bos);
+            mListener.onResult(key, bos.toByteArray());
+        } catch (RemoteException e) {
+            Slog.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
+        } catch (IOException e) {
+            Slog.w(CarLog.TAG_TELEMETRY, "failed to write bundle to output stream", e);
         }
     }
 
-    @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;
+    private void sendError(MetricsConfigKey key, TelemetryProto.TelemetryError error) {
         try {
-            metricsConfig = TelemetryProto.MetricsConfig.parseFrom(configProto);
-        } catch (InvalidProtocolBufferException e) {
-            Slog.e(TAG, "Failed to parse MetricsConfig.", e);
-            return ERROR_PARSE_MANIFEST_FAILED;
+            mListener.onError(key, error.toByteArray());
+        } catch (RemoteException e) {
+            Slog.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
         }
-        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;
+    @VisibleForTesting
+    Handler getTelemetryHandler() {
+        return mTelemetryHandler;
     }
 
-    @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
+    ResultStore getResultStore() {
+        return mResultStore;
+    }
+
+    @VisibleForTesting
+    MetricsConfigStore getMetricsConfigStore() {
+        return mMetricsConfigStore;
     }
 }
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..ff17d0e
--- /dev/null
+++ b/service/src/com/android/car/telemetry/MetricsConfigStore.java
@@ -0,0 +1,120 @@
+/*
+ * 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.
+ */
+public 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;
+
+    public 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.
+     */
+    public 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.
+     */
+    public 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;
+        }
+        mActiveConfigs.put(metricsConfig.getName(), metricsConfig);
+        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. */
+    public boolean deleteMetricsConfig(String metricsConfigName) {
+        mActiveConfigs.remove(metricsConfigName);
+        mNameVersionMap.remove(metricsConfigName);
+        return new File(mConfigDirectory, metricsConfigName).delete();
+    }
+
+    /** Deletes all MetricsConfigs from disk. */
+    public void deleteAllMetricsConfigs() {
+        mActiveConfigs.clear();
+        for (File file : mConfigDirectory.listFiles()) {
+            file.delete();
+        }
+    }
+}
diff --git a/service/src/com/android/car/telemetry/ResultStore.java b/service/src/com/android/car/telemetry/ResultStore.java
new file mode 100644
index 0000000..9c8b2fe
--- /dev/null
+++ b/service/src/com/android/car/telemetry/ResultStore.java
@@ -0,0 +1,266 @@
+/*
+ * 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 android.os.PersistableBundle;
+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.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 {
+
+    private static final long STALE_THRESHOLD_MILLIS =
+            TimeUnit.MILLISECONDS.convert(30, TimeUnit.DAYS);
+    @VisibleForTesting
+    static final String INTERIM_RESULT_DIR = "interim";
+    @VisibleForTesting
+    static final String ERROR_RESULT_DIR = "error";
+    @VisibleForTesting
+    static final String FINAL_RESULT_DIR = "final";
+
+    /** Map keys are MetricsConfig names, which are also the file names in disk. */
+    private final Map<String, InterimResult> mInterimResultCache = new ArrayMap<>();
+
+    private final File mInterimResultDirectory;
+    private final File mErrorResultDirectory;
+    private final File mFinalResultDirectory;
+
+    ResultStore(File rootDirectory) {
+        mInterimResultDirectory = new File(rootDirectory, INTERIM_RESULT_DIR);
+        mErrorResultDirectory = new File(rootDirectory, ERROR_RESULT_DIR);
+        mFinalResultDirectory = new File(rootDirectory, FINAL_RESULT_DIR);
+        mInterimResultDirectory.mkdirs();
+        mErrorResultDirectory.mkdirs();
+        mFinalResultDirectory.mkdirs();
+        // load results into memory to reduce the frequency of disk access
+        loadInterimResultsIntoMemory();
+    }
+
+    /** Reads interim results into memory for faster access. */
+    private void loadInterimResultsIntoMemory() {
+        for (File file : mInterimResultDirectory.listFiles()) {
+            try (FileInputStream fis = new FileInputStream(file)) {
+                mInterimResultCache.put(
+                        file.getName(),
+                        new InterimResult(PersistableBundle.readFromStream(fis)));
+            } catch (IOException e) {
+                Slog.w(CarLog.TAG_TELEMETRY, "Failed to read result from disk.", e);
+                // TODO(b/197153560): record failure
+            }
+        }
+    }
+
+    /**
+     * Retrieves interim metrics for the given
+     * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
+     */
+    public PersistableBundle getInterimResult(String metricsConfigName) {
+        if (!mInterimResultCache.containsKey(metricsConfigName)) {
+            return null;
+        }
+        return mInterimResultCache.get(metricsConfigName).getBundle();
+    }
+
+    /**
+     * Stores interim metrics results in memory for the given
+     * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
+     */
+    public void putInterimResult(String metricsConfigName, PersistableBundle result) {
+        mInterimResultCache.put(metricsConfigName, new InterimResult(result, /* dirty = */ true));
+    }
+
+    /**
+     * Retrieves final metrics for the given
+     * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
+     *
+     * @param metricsConfigName name of the MetricsConfig.
+     * @param deleteResult      if true, the final result will be deleted from disk.
+     * @return the final result as PersistableBundle if exists, null otherwise
+     */
+    public PersistableBundle getFinalResult(String metricsConfigName, boolean deleteResult) {
+        File file = new File(mFinalResultDirectory, metricsConfigName);
+        // if no final result exists for this metrics config, return immediately
+        if (!file.exists()) {
+            return null;
+        }
+        PersistableBundle result = null;
+        try (FileInputStream fis = new FileInputStream(file)) {
+            result = PersistableBundle.readFromStream(fis);
+        } catch (IOException e) {
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to get final result from disk.", e);
+        }
+        if (deleteResult) {
+            file.delete();
+        }
+        return result;
+    }
+
+    /**
+     * Stores final metrics in memory for the given
+     * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
+     */
+    public void putFinalResult(String metricsConfigName, PersistableBundle result) {
+        writePersistableBundleToFile(mFinalResultDirectory, metricsConfigName, result);
+        deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
+        mInterimResultCache.remove(metricsConfigName);
+    }
+
+    /** Returns the error result produced by the metrics config if exists, null otherwise. */
+    public TelemetryProto.TelemetryError getError(
+            String metricsConfigName, boolean deleteResult) {
+        File file = new File(mErrorResultDirectory, metricsConfigName);
+        // if no error exists for this metrics config, return immediately
+        if (!file.exists()) {
+            return null;
+        }
+        TelemetryProto.TelemetryError result = null;
+        try {
+            byte[] serializedBytes = Files.readAllBytes(file.toPath());
+            result = TelemetryProto.TelemetryError.parseFrom(serializedBytes);
+        } catch (IOException e) {
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to get error result from disk.", e);
+        }
+        if (deleteResult) {
+            file.delete();
+        }
+        return result;
+    }
+
+    /** Stores the error object produced by the script. */
+    public void putError(String metricsConfigName, TelemetryProto.TelemetryError error) {
+        mInterimResultCache.remove(metricsConfigName);
+        try {
+            Files.write(
+                    new File(mErrorResultDirectory, metricsConfigName).toPath(),
+                    error.toByteArray());
+            deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
+            mInterimResultCache.remove(metricsConfigName);
+        } catch (IOException e) {
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to write data to file", e);
+            // TODO(b/197153560): record failure
+        }
+    }
+
+    /** Persists data to disk. */
+    public void flushToDisk() {
+        writeInterimResultsToFile();
+        deleteAllStaleData(mInterimResultDirectory, mFinalResultDirectory);
+    }
+
+    /**
+     * Deletes script result associated with the given config name. If result does not exist, this
+     * method does not do anything.
+     */
+    public void deleteResult(String metricsConfigName) {
+        mInterimResultCache.remove(metricsConfigName);
+        deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
+        deleteFileInDirectory(mFinalResultDirectory, metricsConfigName);
+    }
+
+    /** Deletes all interim and final results stored in disk. */
+    public void deleteAllResults() {
+        mInterimResultCache.clear();
+        for (File interimResult : mInterimResultDirectory.listFiles()) {
+            interimResult.delete();
+        }
+        for (File finalResult : mFinalResultDirectory.listFiles()) {
+            finalResult.delete();
+        }
+    }
+
+    /** Writes dirty interim results to disk. */
+    private void writeInterimResultsToFile() {
+        mInterimResultCache.forEach((metricsConfigName, interimResult) -> {
+            // only write dirty data
+            if (!interimResult.isDirty()) {
+                return;
+            }
+            writePersistableBundleToFile(
+                    mInterimResultDirectory, metricsConfigName, interimResult.getBundle());
+        });
+    }
+
+    /** Deletes data that are older than some threshold in the given directories. */
+    private void deleteAllStaleData(File... dirs) {
+        long currTimeMs = System.currentTimeMillis();
+        for (File dir : dirs) {
+            for (File file : dir.listFiles()) {
+                // delete stale data
+                if (file.lastModified() + STALE_THRESHOLD_MILLIS < currTimeMs) {
+                    file.delete();
+                }
+            }
+        }
+    }
+
+    /**
+     * Converts a {@link PersistableBundle} into byte array and saves the results to a file.
+     */
+    private void writePersistableBundleToFile(
+            File dir, String metricsConfigName, PersistableBundle result) {
+        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/197153560): record failure
+        }
+    }
+
+    /** Deletes a the given file in the given directory if it exists. */
+    private void deleteFileInDirectory(File interimResultDirectory, String metricsConfigName) {
+        File file = new File(interimResultDirectory, metricsConfigName);
+        file.delete();
+    }
+
+    /** Wrapper around a result and whether the result should be written to disk. */
+    static final class InterimResult {
+        private final PersistableBundle mBundle;
+        private final boolean mDirty;
+
+        InterimResult(PersistableBundle bundle) {
+            mBundle = bundle;
+            mDirty = false;
+        }
+
+        InterimResult(PersistableBundle bundle, boolean dirty) {
+            mBundle = bundle;
+            mDirty = dirty;
+        }
+
+        PersistableBundle getBundle() {
+            return mBundle;
+        }
+
+        boolean isDirty() {
+            return mDirty;
+        }
+    }
+}
diff --git a/service/src/com/android/car/telemetry/ScriptExecutor.java b/service/src/com/android/car/telemetry/ScriptExecutor.java
deleted file mode 100644
index d791481..0000000
--- a/service/src/com/android/car/telemetry/ScriptExecutor.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.telemetry;
-
-import android.app.Service;
-import android.car.telemetry.IScriptExecutor;
-import android.car.telemetry.IScriptExecutorListener;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.util.Log;
-
-import com.android.car.CarServiceUtils;
-
-/**
- * Executes Lua code in an isolated process with provided source code
- * and input arguments.
- */
-public final class ScriptExecutor extends Service {
-
-    static {
-        System.loadLibrary("scriptexecutorjni");
-    }
-
-    private static final String TAG = ScriptExecutor.class.getSimpleName();
-
-    // Dedicated "worker" thread to handle all calls related to native code.
-    private HandlerThread mNativeHandlerThread;
-    // Handler associated with the native worker thread.
-    private Handler mNativeHandler;
-
-    private final class IScriptExecutorImpl extends IScriptExecutor.Stub {
-        @Override
-        public void invokeScript(String scriptBody, String functionName, Bundle publishedData,
-                Bundle savedState, IScriptExecutorListener listener) {
-            mNativeHandler.post(() ->
-                    nativeInvokeScript(mLuaEnginePtr, scriptBody, functionName, publishedData,
-                            savedState, listener));
-        }
-    }
-    private IScriptExecutorImpl mScriptExecutorBinder;
-
-    // Memory location of Lua Engine object which is allocated in native code.
-    private long mLuaEnginePtr;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-
-        mNativeHandlerThread = CarServiceUtils.getHandlerThread(
-            ScriptExecutor.class.getSimpleName());
-        // TODO(b/192284628): Remove this once start handling all recoverable errors via onError
-        // callback.
-        mNativeHandlerThread.setUncaughtExceptionHandler((t, e) -> Log.e(TAG, e.getMessage()));
-        mNativeHandler = new Handler(mNativeHandlerThread.getLooper());
-
-        mLuaEnginePtr = nativeInitLuaEngine();
-        mScriptExecutorBinder = new IScriptExecutorImpl();
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        nativeDestroyLuaEngine(mLuaEnginePtr);
-        mNativeHandlerThread.quit();
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mScriptExecutorBinder;
-    }
-
-    /**
-    * Initializes Lua Engine.
-    *
-    * <p>Returns memory location of Lua Engine.
-    */
-    private native long nativeInitLuaEngine();
-
-    /**
-     * Destroys LuaEngine at the provided memory address.
-     */
-    private native void nativeDestroyLuaEngine(long luaEnginePtr);
-
-    /**
-     * Calls provided Lua function.
-     *
-     * @param luaEnginePtr memory address of the stored LuaEngine instance.
-     * @param scriptBody complete body of Lua script that also contains the function to be invoked.
-     * @param functionName the name of the function to execute.
-     * @param publishedData input data provided by the source which the function handles.
-     * @param savedState key-value pairs preserved from the previous invocation of the function.
-     * @param listener callback for the sandboxed environent to report back script execution results
-     * and errors.
-     */
-    private native void nativeInvokeScript(long luaEnginePtr, String scriptBody,
-            String functionName, Bundle publishedData, Bundle savedState,
-            IScriptExecutorListener listener);
-}
diff --git a/service/src/com/android/car/telemetry/databroker/DataBroker.java b/service/src/com/android/car/telemetry/databroker/DataBroker.java
index 44f2f61..d7cc2e6 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBroker.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBroker.java
@@ -22,6 +22,18 @@
 public interface DataBroker {
 
     /**
+     * Interface for receiving notification that script finished.
+     */
+    interface ScriptFinishedCallback {
+        /**
+         * Listens to script finished event.
+         *
+         * @param configName the name of the config whose script finished.
+         */
+        void onScriptFinished(String configName);
+    }
+
+    /**
      * Adds an active {@link com.android.car.telemetry.TelemetryProto.MetricsConfig} that is pending
      * execution. When updating the MetricsConfig to a newer version, the caller must call
      * {@link #removeMetricsConfiguration(TelemetryProto.MetricsConfig)} first to clear the old
@@ -29,29 +41,40 @@
      * TODO(b/191378559): Define behavior when metricsConfig contains invalid config
      *
      * @param metricsConfig to be added and queued for execution.
-     * @return true for success, false for failure.
      */
-    boolean addMetricsConfiguration(TelemetryProto.MetricsConfig metricsConfig);
+    void addMetricsConfiguration(TelemetryProto.MetricsConfig metricsConfig);
 
     /**
      * Removes a {@link com.android.car.telemetry.TelemetryProto.MetricsConfig} and all its
      * relevant subscriptions.
      *
      * @param metricsConfig to be removed from DataBroker.
-     * @return true for success, false for failure.
      */
-    boolean removeMetricsConfiguration(TelemetryProto.MetricsConfig metricsConfig);
+    void removeMetricsConfiguration(TelemetryProto.MetricsConfig metricsConfig);
+
+    /**
+     * Adds a {@link ScriptExecutionTask} to the priority queue. This method will schedule the
+     * next task if a task is not currently running.
+     */
+    void addTaskToQueue(ScriptExecutionTask task);
+
+    /**
+     * Checks system health state and executes a task if condition allows.
+     */
+    void scheduleNextTask();
 
     /**
      * Sets callback for notifying script finished.
      *
      * @param callback script finished callback.
      */
-    void setOnScriptFinishedCallback(DataBrokerController.ScriptFinishedCallback callback);
+    void setOnScriptFinishedCallback(ScriptFinishedCallback callback);
 
     /**
-     * Invoked by controller to indicate system health state and which subscribers can be consumed.
-     * A smaller priority number indicates higher priority. Range 1 - 100.
+     * Sets the priority which affects which subscribers can consume data. Invoked by controller to
+     * indicate system health state and which subscribers can be consumed. If controller does not
+     * set the priority, it will be defaulted to 1. A smaller priority number indicates higher
+     * priority. Range 1 - 100.
      */
     void setTaskExecutionPriority(int priority);
 }
diff --git a/service/src/com/android/car/telemetry/databroker/DataBrokerController.java b/service/src/com/android/car/telemetry/databroker/DataBrokerController.java
index 7663667..8df74be 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBrokerController.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBrokerController.java
@@ -16,10 +16,17 @@
 
 package com.android.car.telemetry.databroker;
 
+import android.os.Handler;
+
+import com.android.car.systeminterface.SystemStateInterface;
+import com.android.car.telemetry.MetricsConfigStore;
+import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.TelemetryProto.MetricsConfig;
 import com.android.car.telemetry.systemmonitor.SystemMonitor;
 import com.android.car.telemetry.systemmonitor.SystemMonitorEvent;
 
+import java.time.Duration;
+
 /**
  * DataBrokerController instantiates the DataBroker and manages what Publishers
  * it can read from based current system states and policies.
@@ -30,27 +37,42 @@
     public static final int TASK_PRIORITY_MED = 50;
     public static final int TASK_PRIORITY_LOW = 100;
 
-    private MetricsConfig mMetricsConfig;
     private final DataBroker mDataBroker;
+    private final Handler mTelemetryHandler;
+    private final MetricsConfigStore mMetricsConfigStore;
     private final SystemMonitor mSystemMonitor;
+    private final SystemStateInterface mSystemStateInterface;
 
-    /**
-     * Interface for receiving notification that script finished.
-     */
-    public interface ScriptFinishedCallback {
-        /**
-         * Listens to script finished event.
-         *
-         * @param configName the name of the config whose script finished.
-         */
-        void onScriptFinished(String configName);
+    public DataBrokerController(
+            DataBroker dataBroker,
+            Handler telemetryHandler,
+            MetricsConfigStore metricsConfigStore,
+            SystemMonitor systemMonitor,
+            SystemStateInterface systemStateInterface) {
+        mDataBroker = dataBroker;
+        mTelemetryHandler = telemetryHandler;
+        mMetricsConfigStore = metricsConfigStore;
+        mSystemMonitor = systemMonitor;
+        mSystemStateInterface = systemStateInterface;
+
+        mDataBroker.setOnScriptFinishedCallback(this::onScriptFinished);
+        mSystemMonitor.setSystemMonitorCallback(this::onSystemMonitorEvent);
+        mSystemStateInterface.scheduleActionForBootCompleted(
+                this::startMetricsCollection, Duration.ZERO);
     }
 
-    public DataBrokerController(DataBroker dataBroker, SystemMonitor systemMonitor) {
-        mDataBroker = dataBroker;
-        mDataBroker.setOnScriptFinishedCallback(this::onScriptFinished);
-        mSystemMonitor = systemMonitor;
-        mSystemMonitor.setSystemMonitorCallback(this::onSystemMonitorEvent);
+    /**
+     * Starts collecting data. Once data is sent by publishers, DataBroker will arrange scripts to
+     * run. This method is called by some thread on executor service, therefore the work needs to
+     * be posted on the telemetry thread.
+     */
+    private void startMetricsCollection() {
+        mTelemetryHandler.post(() -> {
+            for (TelemetryProto.MetricsConfig config :
+                    mMetricsConfigStore.getActiveMetricsConfigs()) {
+                mDataBroker.addMetricsConfiguration(config);
+            }
+        });
     }
 
     /**
@@ -59,8 +81,7 @@
      * @param metricsConfig the metrics config.
      */
     public void onNewMetricsConfig(MetricsConfig metricsConfig) {
-        mMetricsConfig = metricsConfig;
-        mDataBroker.addMetricsConfiguration(mMetricsConfig);
+        mDataBroker.addMetricsConfiguration(metricsConfig);
     }
 
     /**
@@ -86,7 +107,7 @@
                 || event.getMemoryUsageLevel() == SystemMonitorEvent.USAGE_LEVEL_HI) {
             mDataBroker.setTaskExecutionPriority(TASK_PRIORITY_HI);
         } else if (event.getCpuUsageLevel() == SystemMonitorEvent.USAGE_LEVEL_MED
-                    || event.getMemoryUsageLevel() == SystemMonitorEvent.USAGE_LEVEL_MED) {
+                || event.getMemoryUsageLevel() == SystemMonitorEvent.USAGE_LEVEL_MED) {
             mDataBroker.setTaskExecutionPriority(TASK_PRIORITY_MED);
         } else {
             mDataBroker.setTaskExecutionPriority(TASK_PRIORITY_LOW);
diff --git a/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java b/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
index 86d725e..f4b3e6a 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
@@ -16,55 +16,213 @@
 
 package com.android.car.telemetry.databroker;
 
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.Slog;
 
 import com.android.car.CarLog;
+import com.android.car.CarServiceUtils;
+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.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.PriorityBlockingQueue;
 
 /**
  * Implementation of the data path component of CarTelemetryService. Forwards the published data
  * from publishers to consumers subject to the Controller's decision.
+ * TODO(b/187743369): Handle thread-safety of member variables.
  */
 public class DataBrokerImpl implements DataBroker {
 
-    // Maps MetricsConfig's name to its subscriptions. This map is useful when removing a
-    // MetricsConfig.
+    private static final int MSG_HANDLE_TASK = 1;
+    private static final int MSG_BIND_TO_SCRIPT_EXECUTOR = 2;
+
+    /** Bind to script executor 5 times before entering disabled state. */
+    private static final int MAX_BIND_SCRIPT_EXECUTOR_ATTEMPTS = 5;
+
+    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 HandlerThread mTelemetryThread = CarServiceUtils.getHandlerThread(
+            CarTelemetryService.class.getSimpleName());
+    private final Handler mTelemetryHandler = new TaskHandler(mTelemetryThread.getLooper());
+
+    /** Thread-safe priority queue for scheduling tasks. */
+    private final PriorityBlockingQueue<ScriptExecutionTask> mTaskQueue =
+            new PriorityBlockingQueue<>();
+
+    /**
+     * Maps MetricsConfig's name to its subscriptions. This map is useful when removing a
+     * MetricsConfig.
+     */
     private final Map<String, List<DataSubscriber>> mSubscriptionMap = new ArrayMap<>();
 
-    private DataBrokerController.ScriptFinishedCallback mScriptFinishedCallback;
-    private final PublisherFactory mPublisherFactory;
+    /**
+     * If something irrecoverable happened, DataBroker should enter into a disabled state to prevent
+     * doing futile work.
+     */
+    private boolean mDisabled = false;
 
-    public DataBrokerImpl(PublisherFactory publisherFactory) {
+    /** Current number of attempts to bind to ScriptExecutor. */
+    private int mBindScriptExecutorAttempts = 0;
+
+    /** Priority of current system to determine if a {@link ScriptExecutionTask} can run. */
+    private int mPriority = 1;
+
+    /** Waiting period between attempts to bind script executor. Can be shortened for tests. */
+    @VisibleForTesting long mBindScriptExecutorDelayMillis = 3_000L;
+
+    /**
+     * 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) {
+            mTelemetryHandler.post(() -> {
+                mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
+                scheduleNextTask();
+            });
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            // TODO(b/198684473): clean up the state after script executor disconnects
+            mTelemetryHandler.post(() -> {
+                mScriptExecutor = null;
+                unbindScriptExecutor();
+            });
+        }
+    };
+
+    public DataBrokerImpl(
+            Context context, PublisherFactory publisherFactory, ResultStore resultStore) {
+        mContext = context;
         mPublisherFactory = publisherFactory;
+        mResultStore = resultStore;
+        mScriptExecutorListener = new ScriptExecutorListener(this);
+        mPublisherFactory.setFailureConsumer(this::onPublisherFailure);
     }
 
-    // current task priority, used to determine which data can be processed
-    private int mTaskExecutionPriority;
+    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 if script executor is nonnull
+        if (mDisabled || mScriptExecutor != null) {
+            return;
+        }
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName(SCRIPT_EXECUTOR_PACKAGE, SCRIPT_EXECUTOR_CLASS));
+        boolean success = mContext.bindServiceAsUser(
+                intent,
+                mServiceConnection,
+                Context.BIND_AUTO_CREATE,
+                UserHandle.SYSTEM);
+        if (success) {
+            mBindScriptExecutorAttempts = 0; // reset
+            return;
+        }
+        unbindScriptExecutor();
+        mBindScriptExecutorAttempts++;
+        if (mBindScriptExecutorAttempts < MAX_BIND_SCRIPT_EXECUTOR_ATTEMPTS) {
+            Slog.w(CarLog.TAG_TELEMETRY,
+                    "failed to get valid connection to ScriptExecutor, retrying in "
+                            + mBindScriptExecutorDelayMillis + "ms.");
+            mTelemetryHandler.sendEmptyMessageDelayed(MSG_BIND_TO_SCRIPT_EXECUTOR,
+                    mBindScriptExecutorDelayMillis);
+        } else {
+            Slog.w(CarLog.TAG_TELEMETRY, "failed to get valid connection to ScriptExecutor, "
+                    + "disabling DataBroker");
+            disableBroker();
+        }
+    }
+
+    /**
+     * 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) {
+            // If ScriptExecutor is gone before unbinding, it will throw this exception
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to unbind from ScriptExecutor", e);
+        }
+    }
+
+    /**
+     * Enters into a disabled state because something irrecoverable happened.
+     * TODO(b/200841260): expose the state to the caller.
+     */
+    private void disableBroker() {
+        mDisabled = true;
+        // remove all MetricConfigs, disable all publishers, stop receiving data
+        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 boolean addMetricsConfiguration(MetricsConfig metricsConfig) {
-        // if metricsConfig already exists, it should not be added again
-        if (mSubscriptionMap.containsKey(metricsConfig.getName())) {
-            return false;
+    public void addMetricsConfiguration(MetricsConfig metricsConfig) {
+        // TODO(b/187743369): pass status back to caller
+        // 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<>();
+        List<DataSubscriber> dataSubscribers = new ArrayList<>(
+                metricsConfig.getSubscribersList().size());
         for (TelemetryProto.Subscriber subscriber : metricsConfig.getSubscribersList()) {
             // protobuf publisher to a concrete Publisher
             AbstractPublisher publisher = mPublisherFactory.getPublisher(
                     subscriber.getPublisher().getPublisherCase());
-
             // create DataSubscriber from TelemetryProto.Subscriber
-            DataSubscriber dataSubscriber = new DataSubscriber(metricsConfig, subscriber);
+            DataSubscriber dataSubscriber = new DataSubscriber(
+                    this,
+                    metricsConfig,
+                    subscriber);
             dataSubscribers.add(dataSubscriber);
 
             try {
@@ -73,17 +231,17 @@
                 publisher.addDataSubscriber(dataSubscriber);
             } catch (IllegalArgumentException e) {
                 Slog.w(CarLog.TAG_TELEMETRY, "Invalid config", e);
-                return false;
+                return;
             }
         }
         mSubscriptionMap.put(metricsConfig.getName(), dataSubscribers);
-        return true;
     }
 
     @Override
-    public boolean removeMetricsConfiguration(MetricsConfig metricsConfig) {
+    public void removeMetricsConfiguration(MetricsConfig metricsConfig) {
+        // TODO(b/187743369): pass status back to caller
         if (!mSubscriptionMap.containsKey(metricsConfig.getName())) {
-            return false;
+            return;
         }
         // get the subscriptions associated with this MetricsConfig, remove it from the map
         List<DataSubscriber> dataSubscribers = mSubscriptionMap.remove(metricsConfig.getName());
@@ -97,23 +255,209 @@
                 // It shouldn't happen, but if happens, let's just log it.
                 Slog.w(CarLog.TAG_TELEMETRY, "Failed to remove subscriber from publisher", e);
             }
-            // TODO(b/187743369): remove related tasks from the queue
         }
-        return true;
+        // Remove all the tasks associated with this metrics config. The underlying impl uses the
+        // weakly consistent iterator, which is thread-safe but does not freeze the collection while
+        // iterating, so it may or may not reflect any updates since the iterator was created.
+        // But since adding & polling from queue should happen in the same thread, the task queue
+        // should not be changed while tasks are being iterated and removed.
+        mTaskQueue.removeIf(task -> task.isAssociatedWithMetricsConfig(metricsConfig));
     }
 
     @Override
-    public void setOnScriptFinishedCallback(DataBrokerController.ScriptFinishedCallback callback) {
+    public void addTaskToQueue(ScriptExecutionTask task) {
+        if (mDisabled) {
+            return;
+        }
+        mTaskQueue.add(task);
+        scheduleNextTask();
+    }
+
+    /**
+     * This method can be called from any thread. It is thread-safe because atomic values and the
+     * blocking queue are thread-safe. It is possible for this method to be invoked from different
+     * threads at the same time, but it is not possible to schedule the same task twice, because
+     * 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.
+     */
+    @Override
+    public void scheduleNextTask() {
+        if (mDisabled || mTelemetryHandler.hasMessages(MSG_HANDLE_TASK)) {
+            return;
+        }
+        mTelemetryHandler.sendEmptyMessage(MSG_HANDLE_TASK);
+    }
+
+    @Override
+    public void setOnScriptFinishedCallback(ScriptFinishedCallback callback) {
+        if (mDisabled) {
+            return;
+        }
         mScriptFinishedCallback = callback;
     }
 
     @Override
     public void setTaskExecutionPriority(int priority) {
-        mTaskExecutionPriority = priority;
+        if (mDisabled) {
+            return;
+        }
+        mPriority = priority;
+        scheduleNextTask(); // when priority updates, schedule a task which checks task queue
     }
 
     @VisibleForTesting
     Map<String, List<DataSubscriber>> getSubscriptionMap() {
-        return mSubscriptionMap;
+        return new ArrayMap<>((ArrayMap<String, List<DataSubscriber>>) mSubscriptionMap);
+    }
+
+    @VisibleForTesting
+    Handler getTelemetryHandler() {
+        return mTelemetryHandler;
+    }
+
+    @VisibleForTesting
+    PriorityBlockingQueue<ScriptExecutionTask> getTaskQueue() {
+        return mTaskQueue;
+    }
+
+    /**
+     * Polls and runs a task from the head of the priority queue if the queue is nonempty and the
+     * head of the queue has priority higher than or equal to the current priority. A higher
+     * priority is denoted by a lower priority number, so head of the queue should have equal or
+     * lower priority number to be polled.
+     */
+    private void pollAndExecuteTask() {
+        // check databroker state is ready to run script
+        if (mDisabled || mCurrentScriptName != null) {
+            return;
+        }
+        // check task is valid and ready to be run
+        ScriptExecutionTask task = mTaskQueue.peek();
+        if (task == null || task.getPriority() > mPriority) {
+            return;
+        }
+        mTaskQueue.poll(); // remove task from queue
+        try {
+            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 {
+                Slog.d(CarLog.TAG_TELEMETRY, "invoking script executor");
+                // 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 = null;
+        }
+    }
+
+    /** Stores final metrics and schedules the next task. */
+    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(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) {
+        mTelemetryHandler.post(() -> {
+            TelemetryProto.TelemetryError.Builder error = TelemetryProto.TelemetryError.newBuilder()
+                    .setErrorType(TelemetryProto.TelemetryError.ErrorType.forNumber(errorType))
+                    .setMessage(message);
+            if (stackTrace != null) {
+                error.setStackTrace(stackTrace);
+            }
+            mResultStore.putError(mCurrentScriptName, error.build());
+            mCurrentScriptName = null;
+            scheduleNextTask();
+        });
+    }
+
+    /** Listens for script execution status. Methods are called on the binder thread. */
+    private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
+        private final WeakReference<DataBrokerImpl> mWeakDataBroker;
+
+        private ScriptExecutorListener(DataBrokerImpl dataBroker) {
+            mWeakDataBroker = new WeakReference<>(dataBroker);
+        }
+
+        @Override
+        public void onScriptFinished(PersistableBundle result) {
+            DataBrokerImpl dataBroker = mWeakDataBroker.get();
+            if (dataBroker == null) {
+                return;
+            }
+            dataBroker.onScriptFinished(result);
+        }
+
+        @Override
+        public void onSuccess(PersistableBundle stateToPersist) {
+            DataBrokerImpl dataBroker = mWeakDataBroker.get();
+            if (dataBroker == null) {
+                return;
+            }
+            dataBroker.onScriptSuccess(stateToPersist);
+        }
+
+        @Override
+        public void onError(int errorType, String message, String stackTrace) {
+            DataBrokerImpl dataBroker = mWeakDataBroker.get();
+            if (dataBroker == null) {
+                return;
+            }
+            dataBroker.onScriptError(errorType, message, stackTrace);
+        }
+    }
+
+    /** Callback handler to handle scheduling and rescheduling of {@link ScriptExecutionTask}s. */
+    class TaskHandler extends Handler {
+        TaskHandler(Looper looper) {
+            super(looper);
+        }
+
+        /**
+         * Handles a message depending on the message ID.
+         * If the msg ID is MSG_HANDLE_TASK, it polls a task from the priority queue and executing a
+         * {@link ScriptExecutionTask}. There are multiple places where this message is sent: when
+         * priority updates, when a new task is added to the priority queue, and when a task
+         * finishes running.
+         */
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                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 0c713c5..9ab7604 100644
--- a/service/src/com/android/car/telemetry/databroker/DataSubscriber.java
+++ b/service/src/com/android/car/telemetry/databroker/DataSubscriber.java
@@ -16,31 +16,84 @@
 
 package com.android.car.telemetry.databroker;
 
-import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.os.SystemClock;
 
 import com.android.car.telemetry.TelemetryProto;
 
+import java.util.Objects;
+
 /**
- * Subscriber class that receive published data and schedules tasks for execution on the data.
+ * Subscriber class that receives published data and schedules tasks for execution.
+ * All methods of this class must be accessed on telemetry thread.
  */
 public class DataSubscriber {
+
+    private final DataBroker mDataBroker;
+    private final TelemetryProto.MetricsConfig mMetricsConfig;
     private final TelemetryProto.Subscriber mSubscriber;
 
-    public DataSubscriber(TelemetryProto.MetricsConfig metricsConfig,
+    public DataSubscriber(
+            DataBroker dataBroker,
+            TelemetryProto.MetricsConfig metricsConfig,
             TelemetryProto.Subscriber subscriber) {
+        mDataBroker = dataBroker;
+        mMetricsConfig = metricsConfig;
         mSubscriber = subscriber;
     }
 
+    /** Returns the handler function name for this subscriber. */
+    public String getHandlerName() {
+        return mSubscriber.getHandler();
+    }
+
     /**
-     * Returns the publisher param {@link com.android.car.telemetry.TelemetryProto.Publisher} that
+     * Returns the publisher param {@link TelemetryProto.Publisher} that
      * contains the data source and the config.
      */
     public TelemetryProto.Publisher getPublisherParam() {
         return mSubscriber.getPublisher();
     }
 
-    /** Pushes data to the subscriber. */
-    public void push(Bundle data) {
-        // TODO(b/187743369): implement
+    /**
+     * Creates a {@link ScriptExecutionTask} and pushes it to the priority queue where the task
+     * will be pending execution.
+     */
+    public void push(PersistableBundle data) {
+        ScriptExecutionTask task = new ScriptExecutionTask(
+                this, data, SystemClock.elapsedRealtime());
+        mDataBroker.addTaskToQueue(task);
+    }
+
+    /** Returns the {@link TelemetryProto.MetricsConfig}. */
+    public TelemetryProto.MetricsConfig getMetricsConfig() {
+        return mMetricsConfig;
+    }
+
+    /** Returns the {@link TelemetryProto.Subscriber}. */
+    public TelemetryProto.Subscriber getSubscriber() {
+        return mSubscriber;
+    }
+
+    /** Returns the priority of subscriber. */
+    public int getPriority() {
+        return mSubscriber.getPriority();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof DataSubscriber)) {
+            return false;
+        }
+        DataSubscriber other = (DataSubscriber) o;
+        return mMetricsConfig.getName().equals(other.getMetricsConfig().getName())
+                && mMetricsConfig.getVersion() == other.getMetricsConfig().getVersion()
+                && mSubscriber.getHandler().equals(other.getSubscriber().getHandler());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mMetricsConfig.getName(), mMetricsConfig.getVersion(),
+                mSubscriber.getHandler());
     }
 }
diff --git a/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java b/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java
new file mode 100644
index 0000000..a1fb522
--- /dev/null
+++ b/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java
@@ -0,0 +1,85 @@
+/*
+ * 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.databroker;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.TelemetryProto;
+
+/**
+ * A wrapper class containing all the necessary information to invoke the ScriptExecutor API. It
+ * is enqueued into the priority queue where it pends execution by {@link DataBroker}.
+ * It implements the {@link Comparable} interface so it has a natural ordering by priority and
+ * creation timestamp in the priority queue.
+ * The object can be accessed from any thread. See {@link DataSubscriber} for thread-safety.
+ */
+public class ScriptExecutionTask implements Comparable<ScriptExecutionTask> {
+    private final long mTimestampMillis;
+    private final DataSubscriber mSubscriber;
+    private final PersistableBundle mData;
+
+    ScriptExecutionTask(DataSubscriber subscriber, PersistableBundle data,
+            long elapsedRealtimeMillis) {
+        mTimestampMillis = elapsedRealtimeMillis;
+        mSubscriber = subscriber;
+        mData = data;
+    }
+
+    /** Returns the priority of the task. */
+    public int getPriority() {
+        return mSubscriber.getPriority();
+    }
+
+    /** Returns the creation timestamp of the task. */
+    public long getCreationTimestampMillis() {
+        return mTimestampMillis;
+    }
+
+    public TelemetryProto.MetricsConfig getMetricsConfig() {
+        return mSubscriber.getMetricsConfig();
+    }
+
+    public String getHandlerName() {
+        return mSubscriber.getHandlerName();
+    }
+
+    public PersistableBundle getData() {
+        return mData;
+    }
+
+    /**
+     * Indicates whether the task is associated with the given
+     * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig).
+     */
+    public boolean isAssociatedWithMetricsConfig(TelemetryProto.MetricsConfig metricsConfig) {
+        return mSubscriber.getMetricsConfig().equals(metricsConfig);
+    }
+
+    @Override
+    public int compareTo(ScriptExecutionTask other) {
+        if (getPriority() < other.getPriority()) {
+            return -1;
+        } else if (getPriority() > other.getPriority()) {
+            return 1;
+        }
+        // if equal priority, compare creation timestamps
+        if (getCreationTimestampMillis() < other.getCreationTimestampMillis()) {
+            return -1;
+        }
+        return 1;
+    }
+}
diff --git a/service/src/com/android/car/telemetry/proto/Android.bp b/service/src/com/android/car/telemetry/proto/Android.bp
index 8ffcaa2..89e09c5 100644
--- a/service/src/com/android/car/telemetry/proto/Android.bp
+++ b/service/src/com/android/car/telemetry/proto/Android.bp
@@ -16,11 +16,16 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-java_library_static {
+java_library {
     name: "cartelemetry-protos",
-    host_supported: true,
     proto: {
         type: "lite",
     },
-    srcs: ["*.proto"],
+    srcs: [
+        ":cartelemetry-cardata-proto-srcs",
+        "atoms.proto",
+        "stats_log.proto",
+        "statsd_config.proto",
+        "telemetry.proto",
+    ],
 }
diff --git a/service/src/com/android/car/telemetry/proto/atoms.proto b/service/src/com/android/car/telemetry/proto/atoms.proto
new file mode 100644
index 0000000..8f14b38
--- /dev/null
+++ b/service/src/com/android/car/telemetry/proto/atoms.proto
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+// Partial clone of frameworks/proto_logging/stats/atoms.proto. CarTelemetryService only uses
+// small number of atoms.
+
+syntax = "proto2";
+
+package android.car.telemetry.statsd;
+option java_package = "com.android.car.telemetry";
+option java_outer_classname = "AtomsProto";
+
+message Atom {
+  oneof pushed {
+    AppStartMemoryStateCaptured app_start_memory_state_captured = 55;
+  }
+
+  // Pulled events will start at field 10000.
+  oneof pulled {
+    ProcessMemoryState process_memory_state = 10013;
+  }
+}
+
+message AppStartMemoryStateCaptured {
+  // The uid if available. -1 means not available.
+  optional int32 uid = 1;
+  optional string process_name = 2;
+  optional string activity_name = 3;
+  optional int64 page_fault = 4;
+  optional int64 page_major_fault = 5;
+  optional int64 rss_in_bytes = 6;
+  optional int64 cache_in_bytes = 7;
+  optional int64 swap_in_bytes = 8;
+}
+
+message ProcessMemoryState {
+  optional int32 uid = 1;
+  optional string process_name = 2;
+  optional int32 oom_adj_score = 3;
+  optional int64 page_fault = 4;
+  optional int64 page_major_fault = 5;
+  optional int64 rss_in_bytes = 6;
+  optional int64 cache_in_bytes = 7;
+  optional int64 swap_in_bytes = 8;
+}
diff --git a/service/src/com/android/car/telemetry/proto/stats_log.proto b/service/src/com/android/car/telemetry/proto/stats_log.proto
new file mode 100644
index 0000000..37d74f9
--- /dev/null
+++ b/service/src/com/android/car/telemetry/proto/stats_log.proto
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+// Partial clone packages/modules/StatsD/statsd/src/stats_log.proto. Unused messages are not copied
+// here.
+
+syntax = "proto2";
+
+package android.car.telemetry.statsd;
+
+option java_package = "com.android.car.telemetry";
+option java_outer_classname = "StatsLogProto";
+
+import "packages/services/Car/service/src/com/android/car/telemetry/proto/atoms.proto";
+
+message DimensionsValue {
+  oneof value {
+    int32 value_int = 3;
+    uint64 value_str_hash = 8;
+  }
+  reserved 1, 2, 4, 5, 6, 7;
+}
+
+message EventMetricData {
+  optional int64 elapsed_timestamp_nanos = 1;
+  optional Atom atom = 2;
+  reserved 3, 4;
+}
+
+message GaugeBucketInfo {
+  repeated Atom atom = 3;
+  repeated int64 elapsed_timestamp_nanos = 4;
+  reserved 1, 2, 5, 6, 7, 8, 9;
+}
+
+message GaugeMetricData {
+  repeated GaugeBucketInfo bucket_info = 3;
+  repeated DimensionsValue dimension_leaf_values_in_what = 4;
+  reserved 1, 2, 5, 6;
+}
+
+message StatsLogReport {
+  optional int64 metric_id = 1;
+
+  message EventMetricDataWrapper {
+    repeated EventMetricData data = 1;
+  }
+
+  message GaugeMetricDataWrapper {
+    repeated GaugeMetricData data = 1;
+    reserved 2;
+  }
+
+  oneof data {
+    EventMetricDataWrapper event_metrics = 4;
+    GaugeMetricDataWrapper gauge_metrics = 8;
+  }
+
+  optional bool is_active = 14;
+
+  reserved 2, 3, 5, 6, 7, 9, 10, 11, 12, 13, 15, 16;
+}
+
+message ConfigMetricsReport {
+  repeated StatsLogReport metrics = 1;
+
+  enum DumpReportReason {
+    DEVICE_SHUTDOWN = 1;
+    CONFIG_UPDATED = 2;
+    CONFIG_REMOVED = 3;
+    GET_DATA_CALLED = 4;
+    ADB_DUMP = 5;
+    CONFIG_RESET = 6;
+    STATSCOMPANION_DIED = 7;
+    TERMINATION_SIGNAL_RECEIVED = 8;
+  }
+  optional DumpReportReason dump_report_reason = 8;
+
+  repeated string strings = 9;
+
+  reserved 2, 3, 4, 5, 6, 7;
+}
+
+message ConfigMetricsReportList {
+  message ConfigKey {
+    optional int32 uid = 1;
+    optional int64 id = 2;
+  }
+  optional ConfigKey config_key = 1;
+
+  repeated ConfigMetricsReport reports = 2;
+
+  reserved 10;
+}
diff --git a/service/src/com/android/car/telemetry/proto/statsd_config.proto b/service/src/com/android/car/telemetry/proto/statsd_config.proto
new file mode 100644
index 0000000..15ea9d5
--- /dev/null
+++ b/service/src/com/android/car/telemetry/proto/statsd_config.proto
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+// Partial clone of packages/modules/StatsD/statsd/src/statsd_config.proto. The service only
+// uses some parameters when configuring StatsD.
+
+syntax = "proto2";
+
+package android.car.telemetry.statsd;
+
+option java_package = "com.android.car.telemetry";
+option java_outer_classname = "StatsdConfigProto";
+
+enum TimeUnit {
+  TIME_UNIT_UNSPECIFIED = 0;
+  FIVE_MINUTES = 2;
+  TEN_MINUTES = 3;
+  THIRTY_MINUTES = 4;
+  ONE_HOUR = 5;
+  reserved 1, 6, 7, 8, 9, 10, 1000;
+}
+
+message FieldMatcher {
+  optional int32 field = 1;
+
+  repeated FieldMatcher child = 3;
+
+  reserved 2;
+}
+
+message SimpleAtomMatcher {
+  optional int32 atom_id = 1;
+
+  reserved 2;
+}
+
+message AtomMatcher {
+  optional int64 id = 1;
+
+  oneof contents {
+    SimpleAtomMatcher simple_atom_matcher = 2;
+  }
+
+  reserved 3;
+}
+
+message FieldFilter {
+  optional bool include_all = 1 [default = false];
+  optional FieldMatcher fields = 2;
+}
+
+message EventMetric {
+  optional int64 id = 1;
+  optional int64 what = 2;
+  optional int64 condition = 3;
+
+  reserved 4;
+  reserved 100;
+  reserved 101;
+}
+
+message GaugeMetric {
+  optional int64 id = 1;
+
+  optional int64 what = 2;
+
+  optional FieldFilter gauge_fields_filter = 3;
+
+  optional FieldMatcher dimensions_in_what = 5;
+
+  optional TimeUnit bucket = 6;
+
+  enum SamplingType {
+    RANDOM_ONE_SAMPLE = 1;
+    CONDITION_CHANGE_TO_TRUE = 3;
+    FIRST_N_SAMPLES = 4;
+    reserved 2;
+  }
+  optional SamplingType sampling_type = 9 [default = RANDOM_ONE_SAMPLE];
+
+  optional int64 max_num_gauge_atoms_per_bucket = 11 [default = 10];
+
+  optional int32 max_pull_delay_sec = 13 [default = 30];
+
+  reserved 4, 7, 8, 10, 12, 14;
+  reserved 100;
+  reserved 101;
+}
+
+message PullAtomPackages {
+  optional int32 atom_id = 1;
+
+  repeated string packages = 2;
+}
+
+message StatsdConfig {
+  optional int64 id = 1;
+
+  repeated EventMetric event_metric = 2;
+
+  repeated GaugeMetric gauge_metric = 5;
+
+  repeated AtomMatcher atom_matcher = 7;
+
+  repeated string allowed_log_source = 12;
+
+  optional int64 ttl_in_seconds = 15;
+
+  optional bool hash_strings_in_metric_report = 16 [default = true];
+
+  optional bool persist_locally = 20 [default = false];
+
+  repeated PullAtomPackages pull_atom_packages = 23;
+
+  repeated int32 whitelisted_atom_ids = 24;
+
+  reserved 3, 4, 6, 8, 9, 10, 11, 13, 14, 17, 18, 19, 21, 22, 25;
+
+  // Do not use.
+  reserved 1000, 1001;
+}
diff --git a/service/src/com/android/car/telemetry/proto/telemetry.proto b/service/src/com/android/car/telemetry/proto/telemetry.proto
index 5787d62..b06f519 100644
--- a/service/src/com/android/car/telemetry/proto/telemetry.proto
+++ b/service/src/com/android/car/telemetry/proto/telemetry.proto
@@ -16,7 +16,7 @@
 
 syntax = "proto2";
 
-package com.android.car.telemetry;
+package android.car.telemetry;
 
 option java_package = "com.android.car.telemetry";
 option java_outer_classname = "TelemetryProto";
@@ -30,16 +30,22 @@
 message MetricsConfig {
   // Required.
   // The name of the configuration. Must be unique within a device.
+  //
+  // Changing the name of the config should be done carefully, by first removing the config
+  // with the old name, and creating a new config with a new name.
+  // The name is used to for persisting the configs and other internal state, not removing the
+  // configs with old name will result dangling data in the system.
+  //
   // Only alphanumeric and _ characters are allowed. Minimum length is 3 chars.
   optional string name = 1;
 
   // Required.
-  // Version number of the configuration.
+  // Version number of the configuration. Must be more than 0.
   optional int32 version = 2;
 
   // Required.
-  // A script that is executed in devices. Must contain all the handler functions
-  // defined in the listeners below.
+  // A script that is executed at the device side. Must contain all the handler
+  // functions defined in the listeners below.
   // The script functions must be `pure` functions.
   optional string script = 3;
 
@@ -60,19 +66,89 @@
   optional float read_rate = 2;
 }
 
+// Parameters for cartelemetryd publisher.
+// See packages/services/Car/cpp/telemetry for CarData proto and docs.
+message CarTelemetrydPublisher {
+  // Required.
+  // CarData id to subscribe to.
+  // See packages/services/Car/cpp/telemetry/proto/CarData.proto for all the
+  // messages and IDs.
+  optional int32 id = 1;
+}
+
+// Publisher for various system metrics and stats.
+// It pushes metrics to the subscribers periodically or when garage mode starts.
+message StatsPublisher {
+  enum SystemMetric {
+    UNDEFINED = 0;  // default value, not used
+    // Collects all the app start events with the initial used RSS/CACHE/SWAP memory.
+    APP_START_MEMORY_STATE_CAPTURED = 1;
+    // Collects memory state of processes in 5-minute buckets (1 memory measurement per bucket).
+    PROCESS_MEMORY_STATE = 2;
+  }
+
+  // Required.
+  // System metric for the publisher.
+  optional SystemMetric system_metric = 1;
+}
+
 // Specifies data publisher and its parameters.
 message Publisher {
   oneof publisher {
     VehiclePropertyPublisher vehicle_property = 1;
+    CarTelemetrydPublisher cartelemetryd = 2;
+    StatsPublisher stats = 3;
   }
 }
 
 // Specifies publisher with its parameters and the handler function that's invoked
 // when data is received. The format of the data depends on the Publisher.
 message Subscriber {
-  // Name of the script function that's invoked when this subscriber is triggered.
+  // Required.
+  // Name of the function that handles the published data. Must be defined
+  // in the script.
   optional string handler = 1;
 
-  // Publisher and its parameters.
+  // Required.
+  // Publisher definition.
   optional Publisher publisher = 2;
+
+  // Required.
+  // Priority of the subscriber, which affects the order of when it receives data.
+  // Ranges from 0 to 100. 0 being highest priority and 100 being lowest.
+  optional int32 priority = 3;
+}
+
+// A message that encapsulates an error that's produced when collecting metrics.
+// Any changes here should also be reflected on
+// p/s/C/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.aidl
+message TelemetryError {
+  enum ErrorType {
+    // Not used.
+    UNSPECIFIED = 0;
+
+    // Used when an error occurs in the ScriptExecutor code.
+    SCRIPT_EXECUTOR_ERROR = 1;
+
+    // Used when an error occurs while executing the Lua script (such as errors returned by
+    // lua_pcall)
+    LUA_RUNTIME_ERROR = 2;
+
+    // Used to log errors by a script itself, for instance, when a script received inputs outside
+    // of expected range.
+    LUA_SCRIPT_ERROR = 3;
+  }
+
+  // Required.
+  // A type that indicates the category of the error.
+  optional ErrorType error_type = 1;
+
+  // Required.
+  // Human readable message explaining the error or how to fix it.
+  optional string message = 2;
+
+  // Optional.
+  // If there is an exception, there will be stack trace. However this information is not always
+  // available.
+  optional string stack_trace = 3;
 }
diff --git a/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java b/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
index f58a6fa..e91831e 100644
--- a/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
+++ b/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
@@ -18,9 +18,7 @@
 
 import com.android.car.telemetry.databroker.DataSubscriber;
 
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
+import java.util.function.BiConsumer;
 
 /**
  * Abstract class for publishers. It is 1-1 with data source and manages sending data to
@@ -30,54 +28,51 @@
  * configuration. Single publisher instance can send data as several
  * {@link com.android.car.telemetry.TelemetryProto.Publisher} to subscribers.
  *
- * <p>Not thread-safe.
+ * <p>Child classes must be called from the telemetry thread.
  */
 public abstract class AbstractPublisher {
-    private final HashSet<DataSubscriber> mDataSubscribers = new HashSet<>();
+    // 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.
      *
+     * <p>DataBroker may call this method when a new {@code MetricsConfig} is added,
+     * {@code CarTelemetryService} is restarted or the device is restarted.
+     *
      * @param subscriber a subscriber to receive data
-     * @throws IllegalArgumentException if an invalid subscriber was provided.
+     * @throws IllegalArgumentException if the subscriber is invalid.
+     * @throws IllegalStateException if there are internal errors.
      */
-    public void addDataSubscriber(DataSubscriber subscriber) {
-        // This method can throw exceptions if data is invalid - do now swap these 2 lines.
-        onDataSubscriberAdded(subscriber);
-        mDataSubscribers.add(subscriber);
-    }
+    public abstract void addDataSubscriber(DataSubscriber subscriber);
 
     /**
      * Removes the subscriber from the publisher. Publisher stops if necessary.
      *
-     * @throws IllegalArgumentException if the subscriber was not found.
+     * <p>It does nothing if subscriber is not found.
      */
-    public void removeDataSubscriber(DataSubscriber subscriber) {
-        if (!mDataSubscribers.remove(subscriber)) {
-            throw new IllegalArgumentException("Failed to remove, subscriber not found");
-        }
-        onDataSubscribersRemoved(Collections.singletonList(subscriber));
-    }
+    public abstract void removeDataSubscriber(DataSubscriber subscriber);
 
     /**
      * Removes all the subscribers from the publisher. The publisher may stop.
+     *
+     * <p>This method also cleans-up internal publisher and the data source persisted state.
      */
-    public void removeAllDataSubscribers() {
-        onDataSubscribersRemoved(mDataSubscribers);
-        mDataSubscribers.clear();
-    }
+    public abstract void removeAllDataSubscribers();
+
+    /** Returns true if the publisher already has this data subscriber. */
+    public abstract boolean hasDataSubscriber(DataSubscriber subscriber);
 
     /**
-     * Called when a new subscriber is added to the publisher.
-     *
-     * @throws IllegalArgumentException if the invalid subscriber was provided.
+     * Notifies the failure consumer that this publisher cannot recover from the hard failure.
+     * For example, it cannot connect to the underlying service.
      */
-    protected abstract void onDataSubscriberAdded(DataSubscriber subscriber);
-
-    /** Called when subscribers are removed from the publisher. */
-    protected abstract void onDataSubscribersRemoved(Collection<DataSubscriber> subscribers);
-
-    protected HashSet<DataSubscriber> getDataSubscribers() {
-        return mDataSubscribers;
+    protected void notifyFailureConsumer(Throwable error) {
+        mFailureConsumer.accept(this, error);
     }
 }
diff --git a/service/src/com/android/car/telemetry/publisher/AtomDataConverter.java b/service/src/com/android/car/telemetry/publisher/AtomDataConverter.java
new file mode 100644
index 0000000..b87218e
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/AtomDataConverter.java
@@ -0,0 +1,265 @@
+/*
+ * 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.publisher;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for converting atom data to {@link PersistableBundle} compatible format.
+ */
+public class AtomDataConverter {
+    static final String UID = "uid";
+    static final String PROCESS_NAME = "process_name";
+    static final String ACTIVITY_NAME = "activity_name";
+    static final String PAGE_FAULT = "page_fault";
+    static final String PAGE_MAJOR_FAULT = "page_major_fault";
+    static final String RSS_IN_BYTES = "rss_in_bytes";
+    static final String CACHE_IN_BYTES = "cache_in_bytes";
+    static final String SWAP_IN_BYTES = "swap_in_bytes";
+    static final String STATE = "state";
+    static final String OOM_ADJ_SCORE = "oom_adj_score";
+
+    /**
+     * Converts a list of atoms to separate the atoms fields values into arrays to be put into the
+     * {@link PersistableBundle}.
+     * The list of atoms must contain atoms of same type.
+     * Only fields with types allowed in {@link PersistableBundle} are added to the bundle.
+     *
+     * @param atoms list of {@link AtomsProto.Atom} of the same type.
+     * @param bundle the {@link PersistableBundle} to hold the converted atom fields.
+     */
+    static void convertAtomsList(List<AtomsProto.Atom> atoms, PersistableBundle bundle) {
+        // The atoms are either pushed or pulled type atoms.
+        switch (atoms.get(0).getPushedCase()) {
+            case APP_START_MEMORY_STATE_CAPTURED:
+                convertAppStartMemoryStateCapturedAtoms(atoms, bundle);
+                break;
+            default:
+                break;
+        }
+        switch (atoms.get(0).getPulledCase()) {
+            case PROCESS_MEMORY_STATE:
+                convertProcessMemoryStateAtoms(atoms, bundle);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Converts {@link AtomsProto.AppStartMemoryStateCaptured} atoms.
+     *
+     * @param atoms the list of {@link AtomsProto.AppStartMemoryStateCaptured} atoms.
+     * @param bundle the {@link PersistableBundle} to hold the converted atom fields.
+     */
+    private static void convertAppStartMemoryStateCapturedAtoms(
+                List<AtomsProto.Atom> atoms, PersistableBundle bundle) {
+        List<Integer> uid = null;
+        List<String> processName = null;
+        List<String> activityName = null;
+        List<Long> pageFault = null;
+        List<Long> pageMajorFault = null;
+        List<Long> rssInBytes = null;
+        List<Long> cacheInBytes = null;
+        List<Long> swapInBytes = null;
+        for (AtomsProto.Atom atom : atoms) {
+            AtomsProto.AppStartMemoryStateCaptured atomData = atom.getAppStartMemoryStateCaptured();
+            // Atom fields may be filtered thus not collected, need to check availability.
+            if (atomData.hasUid()) {
+                if (uid == null) {
+                    uid = new ArrayList();
+                }
+                uid.add(atomData.getUid());
+            }
+            if (atomData.hasProcessName()) {
+                if (processName == null) {
+                    processName = new ArrayList<>();
+                }
+                processName.add(atomData.getProcessName());
+            }
+            if (atomData.hasActivityName()) {
+                if (activityName == null) {
+                    activityName = new ArrayList<>();
+                }
+                activityName.add(atomData.getActivityName());
+            }
+            if (atomData.hasPageFault()) {
+                if (pageFault == null) {
+                    pageFault = new ArrayList<>();
+                }
+                pageFault.add(atomData.getPageFault());
+            }
+            if (atomData.hasPageMajorFault()) {
+                if (pageMajorFault == null) {
+                    pageMajorFault = new ArrayList<>();
+                }
+                pageMajorFault.add(atomData.getPageMajorFault());
+            }
+            if (atomData.hasRssInBytes()) {
+                if (rssInBytes == null) {
+                    rssInBytes = new ArrayList<>();
+                }
+                rssInBytes.add(atomData.getRssInBytes());
+            }
+            if (atomData.hasCacheInBytes()) {
+                if (cacheInBytes == null) {
+                    cacheInBytes = new ArrayList<>();
+                }
+                cacheInBytes.add(atomData.getCacheInBytes());
+            }
+            if (atomData.hasSwapInBytes()) {
+                if (swapInBytes == null) {
+                    swapInBytes = new ArrayList<>();
+                }
+                swapInBytes.add(atomData.getSwapInBytes());
+            }
+        }
+        if (uid != null) {
+            bundle.putIntArray(UID, uid.stream().mapToInt(i -> i).toArray());
+        }
+        if (processName != null) {
+            bundle.putStringArray(
+                    PROCESS_NAME, processName.toArray(new String[0]));
+        }
+        if (activityName != null) {
+            bundle.putStringArray(
+                    ACTIVITY_NAME, activityName.toArray(new String[0]));
+        }
+        if (pageFault != null) {
+            bundle.putLongArray(PAGE_FAULT, pageFault.stream().mapToLong(i -> i).toArray());
+        }
+        if (pageMajorFault != null) {
+            bundle.putLongArray(
+                    PAGE_MAJOR_FAULT, pageMajorFault.stream().mapToLong(i -> i).toArray());
+        }
+        if (rssInBytes != null) {
+            bundle.putLongArray(RSS_IN_BYTES, rssInBytes.stream().mapToLong(i -> i).toArray());
+        }
+        if (cacheInBytes != null) {
+            bundle.putLongArray(
+                    CACHE_IN_BYTES, cacheInBytes.stream().mapToLong(i -> i).toArray());
+        }
+        if (swapInBytes != null) {
+            bundle.putLongArray(
+                    SWAP_IN_BYTES, swapInBytes.stream().mapToLong(i -> i).toArray());
+        }
+    }
+
+    /**
+     * Converts {@link AtomsProto.ProcessMemoryState} atoms.
+     *
+     * @param atoms the list of {@link AtomsProto.ProcessMemoryState} atoms.
+     * @param bundle the {@link PersistableBundle} to hold the converted atom fields.
+     */
+    private static void convertProcessMemoryStateAtoms(
+                List<AtomsProto.Atom> atoms, PersistableBundle bundle) {
+        List<Integer> uid = null;
+        List<String> processName = null;
+        List<Integer> oomAdjScore = null;
+        List<Long> pageFault = null;
+        List<Long> pageMajorFault = null;
+        List<Long> rssInBytes = null;
+        List<Long> cacheInBytes = null;
+        List<Long> swapInBytes = null;
+        for (AtomsProto.Atom atom : atoms) {
+            AtomsProto.ProcessMemoryState atomData = atom.getProcessMemoryState();
+            // Atom fields may be filtered thus not collected, need to check availability.
+            if (atomData.hasUid()) {
+                if (uid == null) {
+                    uid = new ArrayList();
+                }
+                uid.add(atomData.getUid());
+            }
+            if (atomData.hasProcessName()) {
+                if (processName == null) {
+                    processName = new ArrayList<>();
+                }
+                processName.add(atomData.getProcessName());
+            }
+            if (atomData.hasOomAdjScore()) {
+                if (oomAdjScore == null) {
+                    oomAdjScore = new ArrayList<>();
+                }
+                oomAdjScore.add(atomData.getOomAdjScore());
+            }
+            if (atomData.hasPageFault()) {
+                if (pageFault == null) {
+                    pageFault = new ArrayList<>();
+                }
+                pageFault.add(atomData.getPageFault());
+            }
+            if (atomData.hasPageMajorFault()) {
+                if (pageMajorFault == null) {
+                    pageMajorFault = new ArrayList<>();
+                }
+                pageMajorFault.add(atomData.getPageMajorFault());
+            }
+            if (atomData.hasRssInBytes()) {
+                if (rssInBytes == null) {
+                    rssInBytes = new ArrayList<>();
+                }
+                rssInBytes.add(atomData.getRssInBytes());
+            }
+            if (atomData.hasCacheInBytes()) {
+                if (cacheInBytes == null) {
+                    cacheInBytes = new ArrayList<>();
+                }
+                cacheInBytes.add(atomData.getCacheInBytes());
+            }
+            if (atomData.hasSwapInBytes()) {
+                if (swapInBytes == null) {
+                    swapInBytes = new ArrayList<>();
+                }
+                swapInBytes.add(atomData.getSwapInBytes());
+            }
+        }
+        if (uid != null) {
+            bundle.putIntArray(UID, uid.stream().mapToInt(i -> i).toArray());
+        }
+        if (processName != null) {
+            bundle.putStringArray(
+                    PROCESS_NAME, processName.toArray(new String[0]));
+        }
+        if (oomAdjScore != null) {
+            bundle.putIntArray(
+                    OOM_ADJ_SCORE, oomAdjScore.stream().mapToInt(i -> i).toArray());
+        }
+        if (pageFault != null) {
+            bundle.putLongArray(PAGE_FAULT, pageFault.stream().mapToLong(i -> i).toArray());
+        }
+        if (pageMajorFault != null) {
+            bundle.putLongArray(
+                    PAGE_MAJOR_FAULT, pageMajorFault.stream().mapToLong(i -> i).toArray());
+        }
+        if (rssInBytes != null) {
+            bundle.putLongArray(RSS_IN_BYTES, rssInBytes.stream().mapToLong(i -> i).toArray());
+        }
+        if (cacheInBytes != null) {
+            bundle.putLongArray(
+                    CACHE_IN_BYTES, cacheInBytes.stream().mapToLong(i -> i).toArray());
+        }
+        if (swapInBytes != null) {
+            bundle.putLongArray(
+                    SWAP_IN_BYTES, swapInBytes.stream().mapToLong(i -> i).toArray());
+        }
+    }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java b/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java
new file mode 100644
index 0000000..490ab83
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java
@@ -0,0 +1,191 @@
+/*
+ * 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.publisher;
+
+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;
+import android.util.Slog;
+
+import com.android.automotive.telemetry.CarDataProto;
+import com.android.car.CarLog;
+import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.databroker.DataSubscriber;
+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).
+ *
+ * <p>When a subscriber is added, the publisher binds to ICarTelemetryInternal and starts listening
+ * for incoming CarData. The matching CarData will be pushed to the subscriber. It unbinds itself
+ * from ICarTelemetryInternal if there are no subscribers.
+ *
+ * <p>See {@code packages/services/Car/cpp/telemetry/cartelemetryd} to learn more about the service.
+ */
+public class CarTelemetrydPublisher extends AbstractPublisher {
+    private static final boolean DEBUG = false;  // STOPSHIP if true
+    private static final String SERVICE_NAME = ICarTelemetryInternal.DESCRIPTOR + "/default";
+    private static final int BINDER_FLAGS = 0;
+
+    private ICarTelemetryInternal mCarTelemetryInternal;
+
+    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(final CarDataInternal[] dataList) throws RemoteException {
+            if (DEBUG) {
+                Slog.d(CarLog.TAG_TELEMETRY,
+                        "Received " + dataList.length + " CarData from cartelemetryd");
+            }
+            // TODO(b/189142577): Create custom Handler and post message to improve performance
+            mTelemetryHandler.post(() -> onCarDataListReceived(dataList));
+        }
+    };
+
+    CarTelemetrydPublisher(BiConsumer<AbstractPublisher, Throwable> failureConsumer,
+            Handler telemetryHandler) {
+        super(failureConsumer);
+        this.mTelemetryHandler = telemetryHandler;
+    }
+
+    /** 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;
+        }
+        IBinder binder = ServiceManager.checkService(SERVICE_NAME);
+        if (binder == null) {
+            notifyFailureConsumer(new IllegalStateException(
+                    "Failed to connect to the ICarTelemetryInternal: service is not ready"));
+            return;
+        }
+        try {
+            binder.linkToDeath(this::onBinderDied, BINDER_FLAGS);
+        } catch (RemoteException e) {
+            notifyFailureConsumer(new IllegalStateException(
+                    "Failed to connect to the ICarTelemetryInternal: linkToDeath failed", e));
+            return;
+        }
+        mCarTelemetryInternal = ICarTelemetryInternal.Stub.asInterface(binder);
+        try {
+            mCarTelemetryInternal.setListener(mListener);
+        } catch (RemoteException e) {
+            binder.unlinkToDeath(this::onBinderDied, BINDER_FLAGS);
+            mCarTelemetryInternal = null;
+            notifyFailureConsumer(new IllegalStateException(
+                    "Failed to connect to the ICarTelemetryInternal: Cannot set CarData listener",
+                    e));
+        }
+    }
+
+    /**
+     * Disconnects from ICarTelemetryInternal service.
+     *
+     * @throws IllegalStateException if fails to clear the listener.
+     */
+    private void disconnectFromCarTelemetryd() {
+        if (mCarTelemetryInternal == null) {
+            return;  // already disconnected
+        }
+        try {
+            mCarTelemetryInternal.clearListener();
+        } catch (RemoteException e) {
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to remove ICarTelemetryInternal listener", e);
+        }
+        mCarTelemetryInternal.asBinder().unlinkToDeath(this::onBinderDied, BINDER_FLAGS);
+        mCarTelemetryInternal = null;
+    }
+
+    @VisibleForTesting
+    boolean isConnectedToCarTelemetryd() {
+        return mCarTelemetryInternal != null;
+    }
+
+    @Override
+    public void addDataSubscriber(DataSubscriber subscriber) {
+        TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+        Preconditions.checkArgument(
+                publisherParam.getPublisherCase()
+                        == TelemetryProto.Publisher.PublisherCase.CARTELEMETRYD,
+                "Subscribers only with CarTelemetryd publisher are supported by this class.");
+        int carDataId = publisherParam.getCartelemetryd().getId();
+        CarDataProto.CarData.PushedCase carDataCase =
+                CarDataProto.CarData.PushedCase.forNumber(carDataId);
+        Preconditions.checkArgument(
+                carDataCase != null
+                        && carDataCase != CarDataProto.CarData.PushedCase.PUSHED_NOT_SET,
+                "Invalid CarData ID " + carDataId
+                        + ". Please see CarData.proto for the list of available IDs.");
+
+        mSubscribers.add(subscriber);
+
+        connectToCarTelemetryd();
+
+        Slogf.d(CarLog.TAG_TELEMETRY, "Subscribing to CarDat.id=%d", carDataId);
+    }
+
+    @Override
+    public void removeDataSubscriber(DataSubscriber subscriber) {
+        mSubscribers.remove(subscriber);
+        if (mSubscribers.isEmpty()) {
+            disconnectFromCarTelemetryd();
+        }
+    }
+
+    @Override
+    public void removeAllDataSubscribers() {
+        mSubscribers.clear();
+        disconnectFromCarTelemetryd();
+    }
+
+    @Override
+    public boolean hasDataSubscriber(DataSubscriber subscriber) {
+        return mSubscribers.contains(subscriber);
+    }
+
+    /**
+     * 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/ConfigMetricsReportListConverter.java b/service/src/com/android/car/telemetry/publisher/ConfigMetricsReportListConverter.java
new file mode 100644
index 0000000..b3f2b39
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/ConfigMetricsReportListConverter.java
@@ -0,0 +1,120 @@
+/*
+ * 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.publisher;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.StatsLogProto;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class for converting metrics report list data to {@link PersistableBundle} compatible format.
+ */
+public class ConfigMetricsReportListConverter {
+    /**
+     * Converts metrics report list to map of metric_id to {@link PersistableBundle} format where
+     * each PersistableBundle containing arrays of metric fields data.
+     *
+     * Example:
+     * Given a ConfigMetricsReportList like this:
+     * {
+     *   reports: {
+     *     metrics: {
+     *       metric_id: 1234
+     *       event_metrics: {
+     *         data: {...}
+     *       }
+     *       metric_id: 2345
+     *       event_metrics: {
+     *         data: {...}
+     *       }
+     *       metric_id: 3456
+     *       gauge_metrics: {
+     *         data: {...}
+     *       }
+     *     }
+     *     metrics: {
+     *       metric_id: 3456
+     *       gauge_metrics: {
+     *         data: {...}
+     *       }
+     *     }
+     *   }
+     * }
+     * Will result in a map of this form:
+     * {
+     *   "1234" : {...}  // PersistableBundle containing metric 1234's data
+     *   "2345" : {...}
+     *   "3456" : {...}
+     * }
+     *
+     * @param reportList the {@link StatsLogProto.ConfigMetricsReportList} to be converted.
+     * @return a {@link PersistableBundle} containing mapping of metric id to metric data.
+     */
+    static Map<Long, PersistableBundle> convert(StatsLogProto.ConfigMetricsReportList reportList) {
+        // Map metric id to StatsLogReport list so that separate reports can be combined.
+        Map<Long, List<StatsLogProto.StatsLogReport>> metricsStatsReportMap = new HashMap<>();
+        // ConfigMetricsReportList is for one config. Normally only 1 report exists unless
+        // the previous report did not upload after shutdown, then at most 2 reports can exist.
+        for (StatsLogProto.ConfigMetricsReport report : reportList.getReportsList()) {
+            // Each statsReport is for a different metric in the report.
+            for (StatsLogProto.StatsLogReport statsReport : report.getMetricsList()) {
+                Long metricId = statsReport.getMetricId();
+                if (!metricsStatsReportMap.containsKey(metricId)) {
+                    metricsStatsReportMap.put(
+                            metricId, new ArrayList<StatsLogProto.StatsLogReport>());
+                }
+                metricsStatsReportMap.get(metricId).add(statsReport);
+            }
+        }
+        Map<Long, PersistableBundle> metricIdBundleMap = new HashMap<>();
+        // For each metric extract the metric data list from the combined stats reports,
+        // convert to bundle data.
+        for (Map.Entry<Long, List<StatsLogProto.StatsLogReport>>
+                    entry : metricsStatsReportMap.entrySet()) {
+            PersistableBundle statsReportBundle = new PersistableBundle();
+            Long metricId = entry.getKey();
+            List<StatsLogProto.StatsLogReport> statsReportList = entry.getValue();
+            switch (statsReportList.get(0).getDataCase()) {
+                case EVENT_METRICS:
+                    List<StatsLogProto.EventMetricData> eventDataList = new ArrayList<>();
+                    for (StatsLogProto.StatsLogReport statsReport : statsReportList) {
+                        eventDataList.addAll(statsReport.getEventMetrics().getDataList());
+                    }
+                    EventMetricDataConverter.convertEventDataList(
+                            eventDataList, statsReportBundle);
+                    break;
+                case GAUGE_METRICS:
+                    List<StatsLogProto.GaugeMetricData> gaugeDataList = new ArrayList<>();
+                    for (StatsLogProto.StatsLogReport statsReport : statsReportList) {
+                        gaugeDataList.addAll(statsReport.getGaugeMetrics().getDataList());
+                    }
+                    GaugeMetricDataConverter.convertGaugeDataList(
+                            gaugeDataList, statsReportBundle);
+                    break;
+                default:
+                    break;
+            }
+            metricIdBundleMap.put(metricId, statsReportBundle);
+        }
+        return metricIdBundleMap;
+    }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/EventMetricDataConverter.java b/service/src/com/android/car/telemetry/publisher/EventMetricDataConverter.java
new file mode 100644
index 0000000..a1faac3
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/EventMetricDataConverter.java
@@ -0,0 +1,59 @@
+/*
+ * 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.publisher;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for converting event metric data to {@link PersistableBundle} compatible format.
+ */
+public class EventMetricDataConverter {
+    static final String ELAPSED_TIME_NANOS = "elapsed_timestamp_nanos";
+
+    /**
+     * Converts a list of {@link StatsLogProto.EventMetricData} to {@link PersistableBundle}
+     * format such that along with the elapsed time array each field of the atom has an associated
+     * array containing the field's data in order received, matching the elapsed time array order.
+     *
+     * Example:
+     * {
+     *   elapsed_timestamp_nanos: [32948395739, 45623453646, ...]
+     *   uid: [1000, 1100, ...]
+     *   ...
+     * }
+     * @param eventDataList the list of {@link StatsLogProto.EventMetricData} to be converted.
+     * @param bundle the {@link PersistableBundle} to hold the converted values.
+     */
+    static void convertEventDataList(
+                List<StatsLogProto.EventMetricData> eventDataList, PersistableBundle bundle) {
+        List<Long> elapsedTimes = new ArrayList<>();
+        List<AtomsProto.Atom> atoms = new ArrayList<>();
+        for (StatsLogProto.EventMetricData eventData : eventDataList) {
+            elapsedTimes.add(eventData.getElapsedTimestampNanos());
+            atoms.add(eventData.getAtom());
+        }
+        AtomDataConverter.convertAtomsList(atoms, bundle);
+        bundle.putLongArray(
+                ELAPSED_TIME_NANOS, elapsedTimes.stream().mapToLong(i -> i).toArray());
+    }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/GaugeMetricDataConverter.java b/service/src/com/android/car/telemetry/publisher/GaugeMetricDataConverter.java
new file mode 100644
index 0000000..66c9b53
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/GaugeMetricDataConverter.java
@@ -0,0 +1,66 @@
+/*
+ * 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.publisher;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for converting gauge metric data to {@link PersistableBundle} compatible format.
+ */
+public class GaugeMetricDataConverter {
+    static final String DIMENSION_DELIMITOR = "-";
+    static final String ELAPSED_TIME = "elapsed_timestamp_nanos";
+
+    /**
+     * Converts a list of {@link StatsLogProto.GaugeMetricData} to {@link PersistableBundle}
+     * format such that along with the elapsed time array each field of the atom has an associated
+     * array containing the field's data in order received, matching the elapsed time array order.
+     * The atoms are extracted out of the each bucket while preserving the order they had in the
+     * bucket.
+     *
+     * Example:
+     * {
+     *   elapsed_timestamp_nanos: [32948395739, 45623453646, ...]
+     *   uid: [1000, 1100, ...]
+     *   ...
+     * }
+     *
+     * @param gaugeDataList the list of {@link StatsLogProto.GaugeMetricData} to be converted.
+     * @param bundle the {@link PersistableBundle} to hold the converted values.
+     */
+    static void convertGaugeDataList(
+            List<StatsLogProto.GaugeMetricData> gaugeDataList, PersistableBundle bundle) {
+        // TODO(b/200064146): translate the dimension strings to get uid and package_name.
+        List<Long> elapsedTimes = new ArrayList<>();
+        List<AtomsProto.Atom> atoms = new ArrayList<>();
+        for (StatsLogProto.GaugeMetricData gaugeData : gaugeDataList) {
+            for (StatsLogProto.GaugeBucketInfo bi : gaugeData.getBucketInfoList()) {
+                elapsedTimes.addAll(bi.getElapsedTimestampNanosList());
+                atoms.addAll(bi.getAtomList());
+            }
+        }
+        AtomDataConverter.convertAtomsList(atoms, bundle);
+        bundle.putLongArray(
+                ELAPSED_TIME, elapsedTimes.stream().mapToLong(i -> i).toArray());
+    }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/HashUtils.java b/service/src/com/android/car/telemetry/publisher/HashUtils.java
new file mode 100644
index 0000000..62eb12c
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/HashUtils.java
@@ -0,0 +1,63 @@
+/*
+ * 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.publisher;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Utility class for computing hash code.
+ *
+ * <p>Most of the methods are copied from {@code external/guava/}.
+ */
+public class HashUtils {
+
+    /**
+     * Returns the hash code of the given string using SHA-256 algorithm. Returns only the first
+     * 8 bytes if the hash code, as SHA-256 is uniformly distributed.
+     */
+    static long sha256(@NonNull String data) {
+        try {
+            return asLong(MessageDigest.getInstance("SHA-256").digest(data.getBytes()));
+        } catch (NoSuchAlgorithmException e) {
+            // unreachable
+            throw new RuntimeException("SHA-256 algorithm not found.", e);
+        }
+    }
+
+    /**
+     * Returns the first eight bytes of {@code hashCode}, converted to a {@code long} value in
+     * little-endian order.
+     *
+     * <p>Copied from Guava's {@code HashCode#asLong()}.
+     *
+     * @throws IllegalStateException if {@code hashCode bytes < 8}
+     */
+    private static long asLong(byte[] hashCode) {
+        Preconditions.checkState(hashCode.length >= 8, "requires >= 8 bytes (it only has %s bytes)",
+                hashCode.length);
+        long retVal = (hashCode[0] & 0xFF);
+        for (int i = 1; i < Math.min(hashCode.length, 8); i++) {
+            retVal |= (hashCode[i] & 0xFFL) << (i * 8);
+        }
+        return retVal;
+    }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/PublisherFactory.java b/service/src/com/android/car/telemetry/publisher/PublisherFactory.java
index 8d0975a..2e08191 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.os.Handler;
+
 import com.android.car.CarPropertyService;
 import com.android.car.telemetry.TelemetryProto;
 
+import java.io.File;
+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 File mRootDirectory;
+    private final Handler mTelemetryHandler;
+    private final StatsManagerProxy mStatsManager;
     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,
+            File rootDirectory) {
         mCarPropertyService = carPropertyService;
+        mTelemetryHandler = handler;
+        mStatsManager = statsManager;
+        mRootDirectory = rootDirectory;
     }
 
-    /** 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, mRootDirectory);
+                    }
+                    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/StatsManagerImpl.java b/service/src/com/android/car/telemetry/publisher/StatsManagerImpl.java
new file mode 100644
index 0000000..b184f28
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/StatsManagerImpl.java
@@ -0,0 +1,44 @@
+/*
+ * 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.publisher;
+
+import android.app.StatsManager;
+import android.app.StatsManager.StatsUnavailableException;
+
+/** Implementation for {@link StatsManagerProxy} */
+public class StatsManagerImpl implements StatsManagerProxy {
+    private final StatsManager mStatsManager;
+
+    public StatsManagerImpl(StatsManager statsManager) {
+        mStatsManager = statsManager;
+    }
+
+    @Override
+    public byte[] getReports(long configKey) throws StatsUnavailableException {
+        return mStatsManager.getReports(configKey);
+    }
+
+    @Override
+    public void addConfig(long configKey, byte[] data) throws StatsUnavailableException {
+        mStatsManager.addConfig(configKey, data);
+    }
+
+    @Override
+    public void removeConfig(long configKey) throws StatsUnavailableException {
+        mStatsManager.removeConfig(configKey);
+    }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/StatsManagerProxy.java b/service/src/com/android/car/telemetry/publisher/StatsManagerProxy.java
new file mode 100644
index 0000000..046ac44
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/StatsManagerProxy.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.telemetry.publisher;
+
+import android.app.StatsManager;
+import android.app.StatsManager.StatsUnavailableException;
+
+/** Proxy for {@link StatsManager}, as it's marked as final and can't be used in tests. */
+public interface StatsManagerProxy {
+    /** See {@link StatsManager#getReports(long)}. */
+    byte[] getReports(long configKey) throws StatsUnavailableException;
+
+    /** See {@link StatsManager#addConfig(long, byte[])}. */
+    void addConfig(long configKey, byte[] data) throws StatsUnavailableException;
+
+    /** See {@link StatsManager#removeConfig(long)}. */
+    void removeConfig(long configKey) throws StatsUnavailableException;
+}
diff --git a/service/src/com/android/car/telemetry/publisher/StatsPublisher.java b/service/src/com/android/car/telemetry/publisher/StatsPublisher.java
new file mode 100644
index 0000000..68fb9bc
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/StatsPublisher.java
@@ -0,0 +1,470 @@
+/*
+ * 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.publisher;
+
+import static com.android.car.telemetry.AtomsProto.Atom.APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER;
+
+import android.app.StatsManager.StatsUnavailableException;
+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.AtomsProto;
+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;
+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.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+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} to clean-up these
+ * configs from StatsD store.
+ */
+public class StatsPublisher extends AbstractPublisher {
+    // These IDs are used in StatsdConfig and ConfigMetricsReport.
+    @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;
+    @VisibleForTesting
+    static final int PROCESS_MEMORY_STATE_MATCHER_ID = 3;
+    @VisibleForTesting
+    static final int PROCESS_MEMORY_STATE_GAUGE_METRIC_ID = 4;
+
+    // The file that contains stats config key and stats config version
+    @VisibleForTesting
+    static final String SAVED_STATS_CONFIGS_FILE = "stats_config_keys_versions";
+
+    private static final Duration PULL_REPORTS_PERIOD = Duration.ofMinutes(10);
+
+    private static final String BUNDLE_CONFIG_KEY_PREFIX = "statsd-publisher-config-id-";
+    private static final String BUNDLE_CONFIG_VERSION_PREFIX = "statsd-publisher-config-version-";
+
+    @VisibleForTesting
+    static final StatsdConfigProto.FieldMatcher PROCESS_MEMORY_STATE_FIELDS_MATCHER =
+            StatsdConfigProto.FieldMatcher.newBuilder()
+                    .setField(
+                            AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER)
+                    .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                            .setField(
+                                    AtomsProto.ProcessMemoryState.OOM_ADJ_SCORE_FIELD_NUMBER))
+                    .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                            .setField(
+                                    AtomsProto.ProcessMemoryState.PAGE_FAULT_FIELD_NUMBER))
+                    .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                            .setField(
+                                    AtomsProto.ProcessMemoryState.PAGE_MAJOR_FAULT_FIELD_NUMBER))
+                    .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                            .setField(
+                                    AtomsProto.ProcessMemoryState.RSS_IN_BYTES_FIELD_NUMBER))
+                    .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                            .setField(
+                                    AtomsProto.ProcessMemoryState.CACHE_IN_BYTES_FIELD_NUMBER))
+                    .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                            .setField(
+                                    AtomsProto.ProcessMemoryState.SWAP_IN_BYTES_FIELD_NUMBER))
+            .build();
+
+    // TODO(b/197766340): remove unnecessary lock
+    private final Object mLock = new Object();
+
+    private final StatsManagerProxy mStatsManager;
+    private final File mSavedStatsConfigsFile;
+    private final Handler mTelemetryHandler;
+
+    // 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<>();
+
+    private final PersistableBundle mSavedStatsConfigs;
+
+    // TODO(b/198331078): Use telemetry thread
+    StatsPublisher(
+            BiConsumer<AbstractPublisher, Throwable> failureConsumer,
+            StatsManagerProxy statsManager,
+            File rootDirectory) {
+        this(failureConsumer, statsManager, rootDirectory, new Handler(Looper.myLooper()));
+    }
+
+    @VisibleForTesting
+    StatsPublisher(
+            BiConsumer<AbstractPublisher, Throwable> failureConsumer,
+            StatsManagerProxy statsManager,
+            File rootDirectory,
+            Handler handler) {
+        super(failureConsumer);
+        mStatsManager = statsManager;
+        mTelemetryHandler = handler;
+        mSavedStatsConfigsFile = new File(rootDirectory, SAVED_STATS_CONFIGS_FILE);
+        mSavedStatsConfigs = loadBundle();
+    }
+
+    /** Loads the PersistableBundle containing stats config keys and versions from disk. */
+    private PersistableBundle loadBundle() {
+        try (FileInputStream fileInputStream = new FileInputStream(mSavedStatsConfigsFile)) {
+            return PersistableBundle.readFromStream(fileInputStream);
+        } catch (IOException e) {
+            // TODO(b/199947533): handle failure
+            Slog.e(CarLog.TAG_TELEMETRY,
+                    "Failed to read file " + mSavedStatsConfigsFile.getAbsolutePath(), e);
+            return new PersistableBundle();
+        }
+    }
+
+    /** Writes the PersistableBundle containing stats config keys and versions to disk. */
+    private void saveBundle() {
+        try (FileOutputStream fileOutputStream = new FileOutputStream(mSavedStatsConfigsFile)) {
+            mSavedStatsConfigs.writeToStream(fileOutputStream);
+        } catch (IOException e) {
+            // TODO(b/199947533): handle failure
+            Slog.e(CarLog.TAG_TELEMETRY,
+                    "Cannot write to " + mSavedStatsConfigsFile.getAbsolutePath()
+                            + ". Added stats config info is lost.", e);
+        }
+    }
+
+    @Override
+    public void addDataSubscriber(DataSubscriber subscriber) {
+        TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+        Preconditions.checkArgument(
+                publisherParam.getPublisherCase() == PublisherCase.STATS,
+                "Subscribers only with StatsPublisher are supported by this class.");
+
+        synchronized (mLock) {
+            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) {
+            for (String key : mSavedStatsConfigs.keySet()) {
+                // filter out all the config versions
+                if (!key.startsWith(BUNDLE_CONFIG_KEY_PREFIX)) {
+                    continue;
+                }
+                // the remaining values are config keys
+                result.add(mSavedStatsConfigs.getLong(key));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Removes the subscriber from the publisher and removes StatsdConfig from StatsD service.
+     * If StatsdConfig is present in Statsd, it removes it even if the subscriber is not present
+     * in the publisher (it happens when subscriber was added before and CarTelemetryService was
+     * restarted and lost publisher state).
+     */
+    @Override
+    public void removeDataSubscriber(DataSubscriber subscriber) {
+        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() {
+        synchronized (mLock) {
+            for (String key : mSavedStatsConfigs.keySet()) {
+                // filter out all the config versions
+                if (!key.startsWith(BUNDLE_CONFIG_KEY_PREFIX)) {
+                    continue;
+                }
+                // the remaining values are config keys
+                long configKey = mSavedStatsConfigs.getLong(key);
+                try {
+                    mStatsManager.removeConfig(configKey);
+                    String bundleVersion = buildBundleConfigVersionKey(configKey);
+                    mSavedStatsConfigs.remove(key);
+                    mSavedStatsConfigs.remove(bundleVersion);
+                } 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.
+                }
+            }
+            saveBundle();
+            mSavedStatsConfigs.clear();
+        }
+        mIsPullingReports.set(false);
+        mTelemetryHandler.removeCallbacks(mPullReportsPeriodically);
+    }
+
+    @Override
+    public boolean hasDataSubscriber(DataSubscriber subscriber) {
+        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 PersistableBundle to store/retrieve configKey associated with the
+     * subscriber.
+     */
+    private static String buildBundleConfigKey(DataSubscriber subscriber) {
+        return BUNDLE_CONFIG_KEY_PREFIX + subscriber.getMetricsConfig().getName() + "-"
+                + subscriber.getSubscriber().getHandler();
+    }
+
+    /**
+     * Returns the key for PersistableBundle to store/retrieve {@link TelemetryProto.MetricsConfig}
+     * version associated with the configKey (which is generated per DataSubscriber).
+     */
+    private static String buildBundleConfigVersionKey(long configKey) {
+        return BUNDLE_CONFIG_VERSION_PREFIX + configKey;
+    }
+
+    /**
+     * This method can be called even if StatsdConfig was added to StatsD service before. It stores
+     * previously added config_keys in the persistable bundle and only updates StatsD when
+     * the MetricsConfig (of CarTelemetryService) has a new version.
+     */
+    @GuardedBy("mLock")
+    private long addStatsConfigLocked(DataSubscriber subscriber) {
+        long configKey = buildConfigKey(subscriber);
+        // Store MetricsConfig (of CarTelemetryService) version per handler_function.
+        String bundleVersion = buildBundleConfigVersionKey(configKey);
+        if (mSavedStatsConfigs.getInt(bundleVersion) != 0) {
+            int currentVersion = mSavedStatsConfigs.getInt(bundleVersion);
+            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 bundleConfigKey = buildBundleConfigKey(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.
+            mStatsManager.addConfig(configKey, config.toByteArray());
+            mSavedStatsConfigs.putInt(bundleVersion, subscriber.getMetricsConfig().getVersion());
+            mSavedStatsConfigs.putLong(bundleConfigKey, configKey);
+            saveBundle();
+        } catch (StatsUnavailableException 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 bundleConfigKey = buildBundleConfigKey(subscriber);
+        long configKey = buildConfigKey(subscriber);
+        // Store MetricsConfig (of CarTelemetryService) version per handler_function.
+        String bundleVersion = buildBundleConfigVersionKey(configKey);
+        try {
+            mStatsManager.removeConfig(configKey);
+            mSavedStatsConfigs.remove(bundleVersion);
+            mSavedStatsConfigs.remove(bundleConfigKey);
+            saveBundle();
+        } 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;
+    }
+
+    /**
+     * Builds StatsdConfig id (aka config_key) using subscriber handler name.
+     *
+     * <p>StatsD uses ConfigKey struct to uniquely identify StatsdConfigs. StatsD ConfigKey consists
+     * of two parts: client uid and config_key number. The StatsdConfig is added to StatsD from
+     * CarService - which has uid=1000. Currently there is no client under uid=1000 and there will
+     * not be config_key collision.
+     */
+    private static long buildConfigKey(DataSubscriber subscriber) {
+        // Not to be confused with statsd metric, this one is a global CarTelemetry metric name.
+        String metricConfigName = subscriber.getMetricsConfig().getName();
+        String handlerFnName = subscriber.getSubscriber().getHandler();
+        return HashUtils.sha256(metricConfigName + "-" + handlerFnName);
+    }
+
+    /** Builds {@link StatsdConfig} proto for given subscriber. */
+    @VisibleForTesting
+    static StatsdConfig buildStatsdConfig(DataSubscriber subscriber, long configId) {
+        TelemetryProto.StatsPublisher.SystemMetric metric =
+                subscriber.getPublisherParam().getStats().getSystemMetric();
+        StatsdConfig.Builder builder = StatsdConfig.newBuilder()
+                // This id is not used in StatsD, but let's make it the same as config_key
+                // just in case.
+                .setId(configId)
+                .addAllowedLogSource("AID_SYSTEM");
+
+        if (metric == TelemetryProto.StatsPublisher.SystemMetric.APP_START_MEMORY_STATE_CAPTURED) {
+            return buildAppStartMemoryStateStatsdConfig(builder);
+        } else if (metric == TelemetryProto.StatsPublisher.SystemMetric.PROCESS_MEMORY_STATE) {
+            return buildProcessMemoryStateStatsdConfig(builder);
+        } else {
+            throw new IllegalArgumentException("Unsupported metric " + metric.name());
+        }
+    }
+
+    private static StatsdConfig buildAppStartMemoryStateStatsdConfig(StatsdConfig.Builder builder) {
+        return builder
+                .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+                        // The id must be unique within StatsdConfig/matchers
+                        .setId(APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID)
+                        .setSimpleAtomMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+                                .setAtomId(APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER)))
+                .addEventMetric(StatsdConfigProto.EventMetric.newBuilder()
+                        // The id must be unique within StatsdConfig/metrics
+                        .setId(APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID)
+                        .setWhat(APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID))
+                .build();
+    }
+
+    private static StatsdConfig buildProcessMemoryStateStatsdConfig(StatsdConfig.Builder builder) {
+        return builder
+                .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+                        // The id must be unique within StatsdConfig/matchers
+                        .setId(PROCESS_MEMORY_STATE_MATCHER_ID)
+                        .setSimpleAtomMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+                                .setAtomId(AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER)))
+                .addGaugeMetric(StatsdConfigProto.GaugeMetric.newBuilder()
+                        // The id must be unique within StatsdConfig/metrics
+                        .setId(PROCESS_MEMORY_STATE_GAUGE_METRIC_ID)
+                        .setWhat(PROCESS_MEMORY_STATE_MATCHER_ID)
+                        .setDimensionsInWhat(StatsdConfigProto.FieldMatcher.newBuilder()
+                                .setField(AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER)
+                                .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                                        .setField(1))  // ProcessMemoryState.uid
+                                .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                                        .setField(2))  // ProcessMemoryState.process_name
+                        )
+                        .setGaugeFieldsFilter(StatsdConfigProto.FieldFilter.newBuilder()
+                                .setFields(PROCESS_MEMORY_STATE_FIELDS_MATCHER)
+                        )  // setGaugeFieldsFilter
+                        .setSamplingType(
+                                StatsdConfigProto.GaugeMetric.SamplingType.RANDOM_ONE_SAMPLE)
+                        .setBucket(StatsdConfigProto.TimeUnit.FIVE_MINUTES)
+                )
+                .addPullAtomPackages(StatsdConfigProto.PullAtomPackages.newBuilder()
+                        .setAtomId(AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER)
+                        .addPackages("AID_SYSTEM"))
+                .build();
+    }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java b/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
index ed0b19b..dbfc002 100644
--- a/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
+++ b/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
@@ -20,24 +20,28 @@
 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;
 import android.util.SparseArray;
 
 import com.android.car.CarLog;
 import com.android.car.CarPropertyService;
 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.util.Preconditions;
 
-import java.util.Collection;
 import java.util.List;
+import java.util.function.BiConsumer;
 
 /**
  * Publisher for Vehicle Property changes, aka {@code CarPropertyService}.
  *
- * <p>TODO(b/187525360): Add car property listener logic
+ * <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.
  */
 public class VehiclePropertyPublisher extends AbstractPublisher {
     private static final boolean DEBUG = false;  // STOPSHIP if true
@@ -46,8 +50,18 @@
     public static final String CAR_PROPERTY_EVENT_KEY = "car_property_event";
 
     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.
+    // Maps property_id to the set of DataSubscriber.
+    private final SparseArray<ArraySet<DataSubscriber>> mCarPropertyToSubscribers =
+            new SparseArray<>();
+
     private final ICarPropertyEventListener mCarPropertyEventListener =
             new ICarPropertyEventListener.Stub() {
                 @Override
@@ -62,17 +76,21 @@
                 }
             };
 
-    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.
-        mCarPropertyList = new SparseArray<>();
-        for (CarPropertyConfig property : mCarPropertyService.getPropertyList()) {
+        List<CarPropertyConfig> propertyList = mCarPropertyService.getPropertyList();
+        mCarPropertyList = new SparseArray<>(propertyList.size());
+        for (CarPropertyConfig property : propertyList) {
             mCarPropertyList.append(property.getPropertyId(), property);
         }
     }
 
     @Override
-    protected void onDataSubscriberAdded(DataSubscriber subscriber) {
+    public void addDataSubscriber(DataSubscriber subscriber) {
         TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
         Preconditions.checkArgument(
                 publisherParam.getPublisherCase()
@@ -88,31 +106,80 @@
                         || config.getAccess()
                         == CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ_WRITE,
                 "No access. Cannot read " + VehiclePropertyIds.toString(propertyId) + ".");
-        mCarPropertyService.registerListener(
-                propertyId,
-                publisherParam.getVehicleProperty().getReadRate(),
-                mCarPropertyEventListener);
+
+        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
-    protected void onDataSubscribersRemoved(Collection<DataSubscriber> subscribers) {
-        // TODO(b/190230611): Remove car property listener
+    public void removeDataSubscriber(DataSubscriber subscriber) {
+        TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+        if (publisherParam.getPublisherCase() != PublisherCase.VEHICLE_PROPERTY) {
+            Slog.w(CarLog.TAG_TELEMETRY,
+                    "Expected VEHICLE_PROPERTY publisher, but received "
+                            + publisherParam.getPublisherCase().name());
+            return;
+        }
+        int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
+
+        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() {
+        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
+    public boolean hasDataSubscriber(DataSubscriber subscriber) {
+        TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+        if (publisherParam.getPublisherCase() != PublisherCase.VEHICLE_PROPERTY) {
+            return false;
+        }
+        int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
+        ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
+        return subscribers != null && subscribers.contains(subscriber);
     }
 
     /**
-     * Called when publisher receives new events. It's called on CarPropertyService's worker
-     * thread.
+     * Called when publisher receives new event. It's executed on a CarPropertyService's
+     * worker thread.
      */
     private void onVehicleEvent(CarPropertyEvent event) {
-        Bundle bundle = new Bundle();
-        bundle.putParcelable(CAR_PROPERTY_EVENT_KEY, event);
-        for (DataSubscriber subscriber : getDataSubscribers()) {
-            TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
-            if (event.getCarPropertyValue().getPropertyId()
-                    != publisherParam.getVehicleProperty().getVehiclePropertyId()) {
-                continue;
+        // 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()));
+            for (DataSubscriber subscriber : subscribersClone) {
+                subscriber.push(bundle);
             }
-            subscriber.push(bundle);
-        }
+        });
     }
 }
diff --git a/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl
new file mode 100644
index 0000000..3101634
--- /dev/null
+++ b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl
@@ -0,0 +1,62 @@
+/*
+ * 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.scriptexecutorinterface;
+
+import android.os.ParcelFileDescriptor;
+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
+ */
+oneway interface IScriptExecutor {
+  /**
+   * Executes a specified function in a provided Lua script with given input arguments.
+   *
+   * @param scriptBody complete body of Lua script that also contains the function to be invoked
+   * @param functionName the name of the function to execute
+   * @param publishedData input data provided by the source which the function handles
+   * @param savedState key-value pairs preserved from the previous invocation of the function
+   * @param listener callback for the sandboxed environment to report back script execution results,
+   * errors, and logs
+   */
+  void invokeScript(String scriptBody,
+                    String functionName,
+                    in PersistableBundle publishedData,
+                    in @nullable PersistableBundle savedState,
+                    in IScriptExecutorListener listener);
+
+  /**
+   * Executes a specified function in a provided Lua script with given input arguments.
+   * This is a specialized version of invokeScript API above for a case when publishedData input
+   * could be potentially large and overflow Binder's buffer.
+   *
+   * @param scriptBody complete body of Lua script that also contains the function to be invoked
+   * @param functionName the name of the function to execute
+   * @param publishedDataFileDescriptor file descriptor which is be used to open a pipe to read
+   * large amount of input data. The input data is then handled by the provided Lua function.
+   * @param savedState key-value pairs preserved from the previous invocation of the function
+   * @param listener callback for the sandboxed environment to report back script execution results,
+   * errors, and logs
+   */
+  void invokeScriptForLargeInput(String scriptBody,
+                    String functionName,
+                    in ParcelFileDescriptor publishedDataFileDescriptor,
+                    in @nullable PersistableBundle savedState,
+                    in IScriptExecutorListener listener);
+}
diff --git a/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.aidl b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.aidl
new file mode 100644
index 0000000..52f4cbe
--- /dev/null
+++ b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.aidl
@@ -0,0 +1,45 @@
+/*
+ * 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.scriptexecutorinterface;
+
+// TODO(b/194324369): Investigate if we could combine it
+// with IScriptExecutorListener.aidl
+
+interface IScriptExecutorConstants {
+  /**
+   * Default error type.
+   */
+  const int ERROR_TYPE_UNSPECIFIED = 0;
+
+  /**
+   * Used when an error occurs in the ScriptExecutor code.
+   */
+  const int ERROR_TYPE_SCRIPT_EXECUTOR_ERROR = 1;
+
+  /**
+   * Used when an error occurs while executing the Lua script (such as
+   * errors returned by lua_pcall)
+   */
+  const int ERROR_TYPE_LUA_RUNTIME_ERROR = 2;
+
+  /**
+   * Used to log errors by a script itself, for instance, when a script received
+   * inputs outside of expected range.
+   */
+  const int ERROR_TYPE_LUA_SCRIPT_ERROR = 3;
+}
+
diff --git a/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
new file mode 100644
index 0000000..dc64732
--- /dev/null
+++ b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
@@ -0,0 +1,56 @@
+/*
+ * 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.scriptexecutorinterface;
+
+import android.os.PersistableBundle;
+
+/**
+ * Listener for {@code IScriptExecutor#invokeScript}.
+ *
+ * An invocation of a script by Script Executor will result in a call of only one
+ * of the three methods below. If a script fully completes its objective, onScriptFinished
+ * is called. If a script's invocation completes normally, onSuccess is called.
+ * onError is called if any error happens before or during script execution and we
+ * should abandon this run of the script.
+ */
+interface IScriptExecutorListener {
+  /**
+   * Called by ScriptExecutor when the script declares itself as "finished".
+   *
+   * @param result final results of the script that will be uploaded.
+   */
+  void onScriptFinished(in PersistableBundle result);
+
+  /**
+   * Called by ScriptExecutor when a function completes successfully and also provides
+   * optional state that the script wants CarTelemetryService to persist.
+   *
+   * @param stateToPersist key-value pairs to persist
+   */
+  void onSuccess(in @nullable PersistableBundle stateToPersist);
+
+  /**
+   * Called by ScriptExecutor to report errors that prevented the script
+   * from running or completing execution successfully.
+   *
+   * @param errorType type of the error message as defined in this aidl file.
+   * @param messsage the human-readable message containing information helpful for analysis or debugging.
+   * @param stackTrace the stack trace of the error if available.
+   */
+  void onError(int errorType, String message, @nullable String stackTrace);
+}
+
diff --git a/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java b/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
index 65bc45b..0dc5563 100644
--- a/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
+++ b/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
@@ -16,13 +16,45 @@
 
 package com.android.car.telemetry.systemmonitor;
 
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Slog;
+
+import com.android.car.CarLog;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
 /**
  * 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 {
 
-    private SystemMonitorCallback mCallback;
+    private static final int NUM_LOADAVG_VALS = 3;
+    private static final float HI_CPU_LOAD_PER_CORE_BASE_LEVEL = 1.0f;
+    private static final float MED_CPU_LOAD_PER_CORE_BASE_LEVEL = 0.5f;
+    private static final float HI_MEM_LOAD_BASE_LEVEL = 0.95f;
+    private static final float MED_MEM_LOAD_BASE_LEVEL = 0.80f;
+    private static final String LOADAVG_PATH = "/proc/loadavg";
+
+    private static final int POLL_INTERVAL_MILLIS = 60000;
+
+    private final Handler mTelemetryHandler;
+
+    private final Context mContext;
+    private final ActivityManager mActivityManager;
+    private final String mLoadavgPath;
+    private final Runnable mSystemLoadRunnable = this::getSystemLoadRepeated;
+
+    @Nullable private SystemMonitorCallback mCallback;
+    private boolean mSystemMonitorRunning = false;
 
     /**
      * Interface for receiving notifications about system monitor changes.
@@ -37,11 +69,162 @@
     }
 
     /**
-     * Sets the callback to notify of system state changes.
+     * Creates a SystemMonitor instance set with default loadavg path.
+     *
+     * @param context the context this is running in.
+     * @param workerHandler a handler for running monitoring jobs.
+     * @return SystemMonitor instance.
+     */
+    public static SystemMonitor create(Context context, Handler workerHandler) {
+        return new SystemMonitor(context, workerHandler, LOADAVG_PATH);
+    }
+
+    @VisibleForTesting
+    SystemMonitor(Context context, Handler telemetryHandler, String loadavgPath) {
+        mContext = context;
+        mTelemetryHandler = telemetryHandler;
+        mActivityManager = (ActivityManager)
+                mContext.getSystemService(Context.ACTIVITY_SERVICE);
+        mLoadavgPath = loadavgPath;
+    }
+
+    /**
+     * Sets the {@link SystemMonitorCallback} to notify of system state changes.
      *
      * @param callback the callback to nofify state changes on.
      */
     public void setSystemMonitorCallback(SystemMonitorCallback callback) {
         mCallback = callback;
+        if (!mSystemMonitorRunning) {
+            startSystemLoadMonitoring();
+        }
+    }
+
+    /**
+     * Unsets the {@link SystemMonitorCallback}.
+     */
+    public void unsetSystemMonitorCallback() {
+        mTelemetryHandler.removeCallbacks(mSystemLoadRunnable);
+        mSystemMonitorRunning = false;
+        mCallback = null;
+    }
+
+    /**
+     * Gets the loadavg data from /proc/loadavg, getting the first 3 averages,
+     * which are 1-min, 5-min and 15-min moving averages respectively.
+     *
+     * Requires Selinux permissions 'open', 'read, 'getattr' to proc_loadavg,
+     * which is set in Car/car_product/sepolicy/private/carservice_app.te.
+     *
+     * @return the {@link CpuLoadavg}.
+     */
+    @VisibleForTesting
+    @Nullable
+    CpuLoadavg getCpuLoad() {
+        try (BufferedReader reader = new BufferedReader(new FileReader(mLoadavgPath))) {
+            String line = reader.readLine();
+            String[] vals = line.split("\\s+", NUM_LOADAVG_VALS + 1);
+            if (vals.length < NUM_LOADAVG_VALS) {
+                Slog.w(CarLog.TAG_TELEMETRY, "Loadavg wrong format");
+                return null;
+            }
+            CpuLoadavg cpuLoadavg = new CpuLoadavg();
+            cpuLoadavg.mOneMinuteVal = Float.parseFloat(vals[0]);
+            cpuLoadavg.mFiveMinutesVal = Float.parseFloat(vals[1]);
+            cpuLoadavg.mFifteenMinutesVal = Float.parseFloat(vals[2]);
+            return cpuLoadavg;
+        } catch (IOException | NumberFormatException ex) {
+            Slog.w(CarLog.TAG_TELEMETRY, "Failed to read loadavg file.", ex);
+            return null;
+        }
+    }
+
+    /**
+     * Gets the {@link ActivityManager.MemoryInfo} for system memory pressure.
+     *
+     * Of the MemoryInfo fields, we will only be using availMem and totalMem,
+     * since lowMemory and threshold are likely deprecated.
+     *
+     * @return {@link MemoryInfo} for the system.
+     */
+    private MemoryInfo getMemoryLoad() {
+        MemoryInfo mi = new ActivityManager.MemoryInfo();
+        mActivityManager.getMemoryInfo(mi);
+        return mi;
+    }
+
+    /**
+     * Sets the CPU usage level for a {@link SystemMonitorEvent}.
+     *
+     * @param event the {@link SystemMonitorEvent}.
+     * @param cpuLoadPerCore the CPU load average per CPU core.
+     */
+    @VisibleForTesting
+    void setEventCpuUsageLevel(SystemMonitorEvent event, double cpuLoadPerCore) {
+        if (cpuLoadPerCore > HI_CPU_LOAD_PER_CORE_BASE_LEVEL) {
+            event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+        } else if (cpuLoadPerCore > MED_CPU_LOAD_PER_CORE_BASE_LEVEL
+                   && cpuLoadPerCore <= HI_CPU_LOAD_PER_CORE_BASE_LEVEL) {
+            event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+        } else {
+            event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+        }
+    }
+
+    /**
+     * Sets the memory usage level for a {@link SystemMonitorEvent}.
+     *
+     * @param event the {@link SystemMonitorEvent}.
+     * @param memLoadRatio ratio of used memory to total memory.
+     */
+    @VisibleForTesting
+    void setEventMemUsageLevel(SystemMonitorEvent event, double memLoadRatio) {
+        if (memLoadRatio > HI_MEM_LOAD_BASE_LEVEL) {
+            event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+        } else if (memLoadRatio > MED_MEM_LOAD_BASE_LEVEL
+                   && memLoadRatio <= HI_MEM_LOAD_BASE_LEVEL) {
+            event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+        } else {
+            event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+        }
+    }
+
+    /**
+     * The Runnable to repeatedly getting system load data with some interval.
+     */
+    private void getSystemLoadRepeated() {
+        try {
+            CpuLoadavg cpuLoadAvg = getCpuLoad();
+            if (cpuLoadAvg == null) {
+                return;
+            }
+            int numProcessors = Runtime.getRuntime().availableProcessors();
+
+            MemoryInfo memInfo = getMemoryLoad();
+
+            SystemMonitorEvent event = new SystemMonitorEvent();
+            setEventCpuUsageLevel(event, cpuLoadAvg.mOneMinuteVal / numProcessors);
+            setEventMemUsageLevel(event, 1 - memInfo.availMem / memInfo.totalMem);
+
+            mCallback.onSystemMonitorEvent(event);
+        } finally {
+            if (mSystemMonitorRunning) {
+                mTelemetryHandler.postDelayed(mSystemLoadRunnable, POLL_INTERVAL_MILLIS);
+            }
+        }
+    }
+
+    /**
+     * Starts system load monitoring.
+     */
+    private void startSystemLoadMonitoring() {
+        mTelemetryHandler.post(mSystemLoadRunnable);
+        mSystemMonitorRunning = true;
+    }
+
+    static final class CpuLoadavg {
+        float mOneMinuteVal;
+        float mFiveMinutesVal;
+        float mFifteenMinutesVal;
     }
 }
diff --git a/service/src/com/android/car/user/AppLifecycleListener.java b/service/src/com/android/car/user/AppLifecycleListener.java
new file mode 100644
index 0000000..153ca85
--- /dev/null
+++ b/service/src/com/android/car/user/AppLifecycleListener.java
@@ -0,0 +1,77 @@
+/*
+ * 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.user;
+
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+
+import com.android.car.CarLog;
+import com.android.internal.os.IResultReceiver;
+import com.android.server.utils.Slogf;
+
+import java.io.PrintWriter;
+
+/**
+ * Helper DTO to hold info about an app-based {@code UserLifecycleListener}
+ */
+final class AppLifecycleListener {
+
+    private static final String TAG = CarLog.tagFor(AppLifecycleListener.class);
+
+    private final DeathRecipient mDeathRecipient;
+
+    public final int uid;
+    public final String packageName;
+    public final IResultReceiver receiver;
+
+    AppLifecycleListener(int uid, String packageName, IResultReceiver receiver,
+            BinderDeathCallback binderDeathCallback) {
+        this.uid = uid;
+        this.packageName = packageName;
+        this.receiver = receiver;
+
+        mDeathRecipient = () -> binderDeathCallback.onBinderDeath(this);
+        Slogf.v(TAG, "linking death recipient %s", mDeathRecipient);
+        try {
+            receiver.asBinder().linkToDeath(mDeathRecipient, /* flags= */ 0);
+        } catch (RemoteException e) {
+            Slogf.wtf(TAG, "Cannot listen to death of %s", mDeathRecipient);
+        }
+    }
+
+    void onDestroy() {
+        Slogf.v(TAG, "onDestroy(): unlinking death recipient %s", mDeathRecipient);
+        receiver.asBinder().unlinkToDeath(mDeathRecipient, /* flags= */ 0);
+    }
+
+    void dump(PrintWriter writer) {
+        writer.printf("uid=%d, pkg=%s\n", uid, packageName);
+    }
+
+    String toShortString() {
+        return uid + "-" + packageName;
+    }
+
+    @Override
+    public String toString() {
+        return "AppLifecycleListener[uid=" + uid + ", pkg=" + packageName + "]";
+    }
+
+    interface BinderDeathCallback {
+        void onBinderDeath(AppLifecycleListener listener);
+    }
+}
diff --git a/service/src/com/android/car/user/CarUserService.java b/service/src/com/android/car/user/CarUserService.java
index 1fde509..00cb893 100644
--- a/service/src/com/android/car/user/CarUserService.java
+++ b/service/src/com/android/car/user/CarUserService.java
@@ -77,6 +77,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.Trace;
@@ -85,11 +86,11 @@
 import android.provider.Settings;
 import android.sysprop.CarProperties;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.EventLog;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
-import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.TimingsTraceLog;
 import android.view.Display;
@@ -195,16 +196,19 @@
     private final Handler mHandler;
 
     /**
-     * List of listeners to be notified on new user activities events.
-     * This collection should be accessed and manipulated by mHandlerThread only.
+     * Internal listeners to be notified on new user activities events.
+     *
+     * <p>This collection should be accessed and manipulated by {@code mHandlerThread} only.
      */
     private final List<UserLifecycleListener> mUserLifecycleListeners = new ArrayList<>();
 
     /**
-     * List of lifecycle listeners by uid.
-     * This collection should be accessed and manipulated by mHandlerThread only.
+     * App listeners to be notified on new user activities events.
+     *
+     * <p>This collection should be accessed and manipulated by {@code mHandlerThread} only.
      */
-    private final SparseArray<IResultReceiver> mAppLifecycleListeners = new SparseArray<>();
+    private final ArrayMap<IBinder, AppLifecycleListener> mAppLifecycleListeners =
+            new ArrayMap<>();
 
     /**
      * User Id for the user switch in process, if any.
@@ -359,8 +363,7 @@
         checkHasDumpPermissionGranted("dump()");
 
         writer.println("*CarUserService*");
-        String indent = "  ";
-        handleDumpListeners(writer, indent);
+        handleDumpListeners(writer);
         writer.printf("User switch UI receiver %s\n", mUserSwitchUiReceiver);
         synchronized (mLockUser) {
             writer.println("User0Unlocked: " + mUser0Unlocked);
@@ -380,9 +383,10 @@
         List<UserInfo> allDrivers = getAllDrivers();
         int driversSize = allDrivers.size();
         writer.println("NumberOfDrivers: " + driversSize);
+        writer.increaseIndent();
         for (int i = 0; i < driversSize; i++) {
             int driverId = allDrivers.get(i).id;
-            writer.print(indent + "#" + i + ": id=" + driverId);
+            writer.printf("#%d: id=%d", i, driverId);
             List<UserInfo> passengers = getPassengers(driverId);
             int passengersSize = passengers.size();
             writer.print(" NumberPassengers: " + passengersSize);
@@ -398,38 +402,42 @@
             }
             writer.println();
         }
+        writer.decreaseIndent();
         writer.printf("EnablePassengerSupport: %s\n", mEnablePassengerSupport);
         writer.printf("User HAL timeout: %dms\n",  mHalTimeoutMs);
         writer.printf("Initial user: %s\n", mInitialUser);
 
         writer.println("Relevant overlayable properties");
         Resources res = mContext.getResources();
-        writer.printf("%sowner_name=%s\n", indent,
-                res.getString(com.android.internal.R.string.owner_name));
-        writer.printf("%sdefault_guest_name=%s\n", indent,
-                res.getString(R.string.default_guest_name));
+        writer.increaseIndent();
+        writer.printf("owner_name=%s\n", res.getString(com.android.internal.R.string.owner_name));
+        writer.printf("default_guest_name=%s\n", res.getString(R.string.default_guest_name));
+        writer.decreaseIndent();
         writer.printf("User switch in process=%d\n", mUserIdForUserSwitchInProcess);
         writer.printf("Request Id for the user switch in process=%d\n ",
                     mRequestIdForUserSwitchInProcess);
         writer.printf("System UI package name=%s\n", getSystemUiPackageName());
 
         writer.println("Relevant Global settings");
-        dumpGlobalProperty(writer, indent, CarSettings.Global.LAST_ACTIVE_USER_ID);
-        dumpGlobalProperty(writer, indent, CarSettings.Global.LAST_ACTIVE_PERSISTENT_USER_ID);
+        writer.increaseIndent();
+        dumpGlobalProperty(writer, CarSettings.Global.LAST_ACTIVE_USER_ID);
+        dumpGlobalProperty(writer, CarSettings.Global.LAST_ACTIVE_PERSISTENT_USER_ID);
+        writer.decreaseIndent();
 
         mInitialUserSetter.dump(writer);
     }
 
-    private void dumpGlobalProperty(PrintWriter writer, String indent, String property) {
+    private void dumpGlobalProperty(IndentingPrintWriter writer, String property) {
         String value = Settings.Global.getString(mContext.getContentResolver(), property);
-        writer.printf("%s%s=%s\n", indent, property, value);
+        writer.printf("%s=%s\n", property, value);
     }
 
-    private void handleDumpListeners(@NonNull PrintWriter writer, String indent) {
+    private void handleDumpListeners(IndentingPrintWriter writer) {
+        writer.increaseIndent();
         CountDownLatch latch = new CountDownLatch(1);
         mHandler.post(() -> {
             handleDumpServiceLifecycleListeners(writer);
-            handleDumpAppLifecycleListeners(writer, indent);
+            handleDumpAppLifecycleListeners(writer);
             latch.countDown();
         });
         int timeout = 5;
@@ -442,9 +450,10 @@
             Thread.currentThread().interrupt();
             writer.println("Interrupted waiting for handler thread to dump app and user listeners");
         }
+        writer.decreaseIndent();
     }
 
-    private void handleDumpServiceLifecycleListeners(@NonNull PrintWriter writer) {
+    private void handleDumpServiceLifecycleListeners(PrintWriter writer) {
         if (mUserLifecycleListeners.isEmpty()) {
             writer.println("No lifecycle listeners for internal services");
             return;
@@ -452,24 +461,24 @@
         int size = mUserLifecycleListeners.size();
         writer.printf("%d lifecycle listener%s for services\n", size, size == 1 ? "" : "s");
         String indent = "  ";
-        for (UserLifecycleListener listener : mUserLifecycleListeners) {
+        for (int i = 0; i < size; i++) {
+            UserLifecycleListener listener = mUserLifecycleListeners.get(i);
             writer.printf("%s%s\n", indent, FunctionalUtils.getLambdaName(listener));
         }
     }
 
-    private void handleDumpAppLifecycleListeners(@NonNull PrintWriter writer, String indent) {
+    private void handleDumpAppLifecycleListeners(IndentingPrintWriter writer) {
         int size = mAppLifecycleListeners.size();
         if (size == 0) {
             writer.println("No lifecycle listeners for apps");
             return;
         }
-        writer.printf("%d lifecycle listener%s for apps \n", size, size == 1 ? "" : "s");
+        writer.printf("%d lifecycle listener%s for apps\n", size, size == 1 ? "" : "s");
+        writer.increaseIndent();
         for (int i = 0; i < size; i++) {
-            int uid = mAppLifecycleListeners.keyAt(i);
-            IResultReceiver listener = mAppLifecycleListeners.valueAt(i);
-            writer.printf("%suid: %d listener: %s\n", indent, uid,
-                    FunctionalUtils.getLambdaName(listener));
+            mAppLifecycleListeners.valueAt(i).dump(writer);
         }
+        writer.decreaseIndent();
     }
 
     /**
@@ -684,30 +693,45 @@
     }
 
     @Override
-    public void setLifecycleListenerForUid(IResultReceiver listener) {
+    public void setLifecycleListenerForApp(String packageName, IResultReceiver receiver) {
         int uid = Binder.getCallingUid();
-        EventLog.writeEvent(EventLogTags.CAR_USER_SVC_SET_LIFECYCLE_LISTENER, uid);
-        checkInteractAcrossUsersPermission("setLifecycleListenerForUid" + uid);
+        EventLog.writeEvent(EventLogTags.CAR_USER_SVC_SET_LIFECYCLE_LISTENER, uid, packageName);
+        checkInteractAcrossUsersPermission("setLifecycleListenerForApp-" + uid + "-" + packageName);
 
-        try {
-            listener.asBinder().linkToDeath(() -> onListenerDeath(uid), 0);
-        } catch (RemoteException e) {
-            Slog.wtf(TAG, "Cannot listen to death of " + uid);
-        }
-        mHandler.post(() -> mAppLifecycleListeners.append(uid, listener));
+        IBinder receiverBinder = receiver.asBinder();
+        AppLifecycleListener listener = new AppLifecycleListener(uid, packageName, receiver,
+                (l) -> onListenerDeath(l));
+        Slogf.d(TAG, "Adding %s (using binder %s)", listener, receiverBinder);
+        mHandler.post(() -> mAppLifecycleListeners.put(receiverBinder, listener));
     }
 
-    private void onListenerDeath(int uid) {
-        Slog.i(TAG, "Removing listeners for uid " + uid + " on binder death");
-        mHandler.post(() -> mAppLifecycleListeners.remove(uid));
+    private void onListenerDeath(AppLifecycleListener listener) {
+        Slogf.i(TAG, "Removing listener %s on binder death", listener);
+        mHandler.post(() -> mAppLifecycleListeners.remove(listener.receiver.asBinder()));
     }
 
     @Override
-    public void resetLifecycleListenerForUid() {
+    public void resetLifecycleListenerForApp(IResultReceiver receiver) {
         int uid = Binder.getCallingUid();
-        EventLog.writeEvent(EventLogTags.CAR_USER_SVC_RESET_LIFECYCLE_LISTENER, uid);
-        checkInteractAcrossUsersPermission("resetLifecycleListenerForUid-" + uid);
-        mHandler.post(() -> mAppLifecycleListeners.remove(uid));
+        checkInteractAcrossUsersPermission("resetLifecycleListenerForApp-" + uid);
+        IBinder receiverBinder = receiver.asBinder();
+        mHandler.post(() -> {
+            AppLifecycleListener listener = mAppLifecycleListeners.get(receiverBinder);
+            if (listener == null) {
+                Slogf.e(TAG, "resetLifecycleListenerForApp(uid=%d): no listener for receiver", uid);
+                return;
+            }
+            if (listener.uid != uid) {
+                Slogf.e(TAG, "resetLifecycleListenerForApp(): uid mismatch (called by %d) for "
+                        + "listener %s", uid, listener);
+            }
+            EventLog.writeEvent(EventLogTags.CAR_USER_SVC_RESET_LIFECYCLE_LISTENER, uid,
+                    listener.packageName);
+            Slogf.d(TAG, "Removing %s (using binder %s)", listener, receiverBinder);
+            mAppLifecycleListeners.remove(receiverBinder);
+
+            listener.onDestroy();
+        });
     }
 
     /**
@@ -2102,23 +2126,17 @@
     private void handleNotifyAppUserLifecycleListeners(UserLifecycleEvent event) {
         int listenersSize = mAppLifecycleListeners.size();
         if (listenersSize == 0) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Slog.d(TAG, "No app listener to be notified of " + event);
-            }
+            Slogf.d(TAG, "No app listener to be notified of %s", event);
             return;
         }
         // Must use a different TimingsTraceLog because it's another thread
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Slog.d(TAG, "Notifying " + listenersSize + " app listeners of " + event);
-        }
+        Slogf.d(TAG, "Notifying %d app listeners of %s", listenersSize, event);
         int userId = event.getUserId();
         TimingsTraceLog t = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
         int eventType = event.getEventType();
         t.traceBegin("notify-app-listeners-user-" + userId + "-event-" + eventType);
         for (int i = 0; i < listenersSize; i++) {
-            int uid = mAppLifecycleListeners.keyAt(i);
-
-            IResultReceiver listener = mAppLifecycleListeners.valueAt(i);
+            AppLifecycleListener listener = mAppLifecycleListeners.valueAt(i);
             Bundle data = new Bundle();
             data.putInt(CarUserManager.BUNDLE_PARAM_ACTION, eventType);
 
@@ -2126,17 +2144,14 @@
             if (fromUserId != UserHandle.USER_NULL) {
                 data.putInt(CarUserManager.BUNDLE_PARAM_PREVIOUS_USER_ID, fromUserId);
             }
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Slog.d(TAG, "Notifying listener for uid " + uid);
-            }
+            Slogf.d(TAG, "Notifying listener %s", listener);
             EventLog.writeEvent(EventLogTags.CAR_USER_SVC_NOTIFY_APP_LIFECYCLE_LISTENER,
-                    uid, eventType, fromUserId, userId);
+                    listener.uid, listener.packageName, eventType, fromUserId, userId);
             try {
-                t.traceBegin("notify-app-listener-uid-" + uid);
-                listener.send(userId, data);
+                t.traceBegin("notify-app-listener-" + listener.toShortString());
+                listener.receiver.send(userId, data);
             } catch (RemoteException e) {
-                Slog.e(TAG, "Error calling lifecycle listener", e);
+                Slogf.e(TAG, e, "Error calling lifecycle listener %s", listener);
             } finally {
                 t.traceEnd();
             }
diff --git a/service/src/com/android/car/watchdog/CarWatchdogService.java b/service/src/com/android/car/watchdog/CarWatchdogService.java
index fccaa8b..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
@@ -326,6 +343,24 @@
         return mWatchdogPerfHandler.getResourceOveruseConfigurations(resourceOveruseFlag);
     }
 
+    /**
+     * Enables/disables the watchdog daemon client health check process.
+     */
+    public void controlProcessHealthCheck(boolean disable) {
+        ICarImpl.assertPermission(mContext, Car.PERMISSION_USE_CAR_WATCHDOG);
+        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) {
@@ -359,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) {
@@ -399,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;
@@ -434,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;
@@ -467,6 +503,11 @@
         mContext.registerReceiverForAllUsers(mBroadcastReceiver, filter, null, null);
     }
 
+    @VisibleForTesting
+    void setResourceOveruseKillingDelay(long millis) {
+        mWatchdogPerfHandler.setResourceOveruseKillingDelay(millis);
+    }
+
     private static final class ICarWatchdogServiceForSystemImpl
             extends ICarWatchdogServiceForSystem.Stub {
         private final WeakReference<CarWatchdogService> mService;
diff --git a/service/src/com/android/car/watchdog/PackageInfoHandler.java b/service/src/com/android/car/watchdog/PackageInfoHandler.java
index f04f2b9..e3f4bb8 100644
--- a/service/src/com/android/car/watchdog/PackageInfoHandler.java
+++ b/service/src/com/android/car/watchdog/PackageInfoHandler.java
@@ -16,6 +16,7 @@
 
 package com.android.car.watchdog;
 
+import android.annotation.Nullable;
 import android.automotive.watchdog.internal.ApplicationCategoryType;
 import android.automotive.watchdog.internal.ComponentType;
 import android.automotive.watchdog.internal.PackageIdentifier;
@@ -25,8 +26,10 @@
 import android.content.pm.PackageManager;
 import android.os.Process;
 import android.os.UserHandle;
+import android.util.ArrayMap;
 import android.util.IntArray;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 
 import com.android.car.CarLog;
 import com.android.internal.annotations.GuardedBy;
@@ -38,13 +41,18 @@
 
 /** Handles package info resolving */
 public final class PackageInfoHandler {
+    public static final String SHARED_PACKAGE_PREFIX = "shared:";
+
     private static final String TAG = CarLog.tagFor(PackageInfoHandler.class);
 
     private final PackageManager mPackageManager;
     private final Object mLock = new Object();
-    /* Cache of uid to package name mapping. */
     @GuardedBy("mLock")
-    private final SparseArray<String> mPackageNamesByUid = new SparseArray<>();
+    private final SparseArray<String> mGenericPackageNameByUid = new SparseArray<>();
+    @GuardedBy("mLock")
+    private final SparseArray<List<String>> mPackagesBySharedUid = new SparseArray<>();
+    @GuardedBy("mLock")
+    private final ArrayMap<String, String> mGenericPackageNameByPackage = new ArrayMap<>();
     @GuardedBy("mLock")
     private List<String> mVendorPackagePrefixes = new ArrayList<>();
 
@@ -53,42 +61,99 @@
     }
 
     /**
-     * Returns package names for the given UIDs.
+     * Returns the generic package names for the given UIDs.
      *
-     * Some UIDs may not have package names. This may occur when a UID is being removed and the
+     * Some UIDs may not have names. This may occur when a UID is being removed and the
      * internal data structures are not up-to-date. The caller should handle it.
      */
-    public SparseArray<String> getPackageNamesForUids(int[] uids) {
+    public SparseArray<String> getNamesForUids(int[] uids) {
         IntArray unmappedUids = new IntArray(uids.length);
-        SparseArray<String> packageNamesByUid = new SparseArray<>();
+        SparseArray<String> genericPackageNameByUid = new SparseArray<>();
         synchronized (mLock) {
             for (int uid : uids) {
-                String packageName = mPackageNamesByUid.get(uid, null);
-                if (packageName != null) {
-                    packageNamesByUid.append(uid, packageName);
+                String genericPackageName = mGenericPackageNameByUid.get(uid, null);
+                if (genericPackageName != null) {
+                    genericPackageNameByUid.append(uid, genericPackageName);
                 } else {
                     unmappedUids.add(uid);
                 }
             }
         }
         if (unmappedUids.size() == 0) {
-            return packageNamesByUid;
+            return genericPackageNameByUid;
         }
-        String[] packageNames = mPackageManager.getNamesForUids(unmappedUids.toArray());
+        String[] genericPackageNames = mPackageManager.getNamesForUids(unmappedUids.toArray());
         synchronized (mLock) {
             for (int i = 0; i < unmappedUids.size(); ++i) {
-                if (packageNames[i] == null || packageNames[i].isEmpty()) {
+                if (genericPackageNames[i] == null || genericPackageNames[i].isEmpty()) {
                     continue;
                 }
-                mPackageNamesByUid.append(unmappedUids.get(i), packageNames[i]);
-                packageNamesByUid.append(unmappedUids.get(i), packageNames[i]);
+                int uid = unmappedUids.get(i);
+                String genericPackageName = genericPackageNames[i];
+                mGenericPackageNameByUid.append(uid, genericPackageName);
+                genericPackageNameByUid.append(uid, genericPackageName);
+                mGenericPackageNameByPackage.put(genericPackageName, genericPackageName);
+                if (!genericPackageName.startsWith(SHARED_PACKAGE_PREFIX)) {
+                    continue;
+                }
+                populateSharedPackagesLocked(uid, genericPackageName);
             }
         }
-        return packageNamesByUid;
+        return genericPackageNameByUid;
     }
 
     /**
-     * Returns package infos for the given UIDs.
+     * Returns the generic package name for the user package.
+     *
+     * Returns null when no generic package name is found.
+     */
+    @Nullable
+    public String getNameForUserPackage(String packageName, int userId) {
+        synchronized (mLock) {
+            String genericPackageName = mGenericPackageNameByPackage.get(packageName);
+            if (genericPackageName != null) {
+                return genericPackageName;
+            }
+        }
+        try {
+            return getNameForPackage(
+                    mPackageManager.getPackageInfoAsUser(packageName, /* flags= */ 0, userId));
+        } catch (PackageManager.NameNotFoundException e) {
+            Slogf.e(TAG, "Package '%s' not found for user %d: %s", packageName, userId, e);
+        }
+        return null;
+    }
+
+    /** Returns the packages owned by the shared UID */
+    public List<String> getPackagesForUid(int uid, String genericPackageName) {
+        synchronized (mLock) {
+            /* When fetching the packages under a shared UID update the internal DS. This will help
+             * capture any recently installed packages.
+             */
+            populateSharedPackagesLocked(uid, genericPackageName);
+            return mPackagesBySharedUid.get(uid);
+        }
+    }
+
+    /** Returns the generic package name for the given package info. */
+    public String getNameForPackage(android.content.pm.PackageInfo packageInfo) {
+        synchronized (mLock) {
+            String genericPackageName = mGenericPackageNameByPackage.get(packageInfo.packageName);
+            if (genericPackageName != null) {
+                return genericPackageName;
+            }
+            if (packageInfo.sharedUserId != null) {
+                populateSharedPackagesLocked(packageInfo.applicationInfo.uid,
+                        SHARED_PACKAGE_PREFIX + packageInfo.sharedUserId);
+                return SHARED_PACKAGE_PREFIX + packageInfo.sharedUserId;
+            }
+            mGenericPackageNameByPackage.put(packageInfo.packageName, packageInfo.packageName);
+            return packageInfo.packageName;
+        }
+    }
+
+    /**
+     * Returns the internal package infos for the given UIDs.
      *
      * Some UIDs may not have package infos. This may occur when a UID is being removed and the
      * internal data structures are not up-to-date. The caller should handle it.
@@ -103,20 +168,29 @@
              */
             mVendorPackagePrefixes = vendorPackagePrefixes;
         }
-        SparseArray<String> packageNamesByUid = getPackageNamesForUids(uids);
-        ArrayList<PackageInfo> packageInfos = new ArrayList<>(packageNamesByUid.size());
-        for (int i = 0; i < packageNamesByUid.size(); ++i) {
-            packageInfos.add(getPackageInfo(packageNamesByUid.keyAt(i),
-                    packageNamesByUid.valueAt(i)));
+        SparseArray<String> genericPackageNameByUid = getNamesForUids(uids);
+        ArrayList<PackageInfo> packageInfos = new ArrayList<>(genericPackageNameByUid.size());
+        for (int i = 0; i < genericPackageNameByUid.size(); ++i) {
+            packageInfos.add(getPackageInfo(genericPackageNameByUid.keyAt(i),
+                    genericPackageNameByUid.valueAt(i)));
         }
         return packageInfos;
     }
 
-    private PackageInfo getPackageInfo(int uid, String packageName) {
+    @GuardedBy("mLock")
+    private void populateSharedPackagesLocked(int uid, String genericPackageName) {
+        String[] packages = mPackageManager.getPackagesForUid(uid);
+        for (String pkg : packages) {
+            mGenericPackageNameByPackage.put(pkg, genericPackageName);
+        }
+        mPackagesBySharedUid.put(uid, Arrays.asList(packages));
+    }
+
+    private PackageInfo getPackageInfo(int uid, String genericPackageName) {
         PackageInfo packageInfo = new PackageInfo();
         packageInfo.packageIdentifier = new PackageIdentifier();
         packageInfo.packageIdentifier.uid = uid;
-        packageInfo.packageIdentifier.name = packageName;
+        packageInfo.packageIdentifier.name = genericPackageName;
         packageInfo.sharedUidPackages = new ArrayList<>();
         packageInfo.componentType = ComponentType.UNKNOWN;
         /* Application category type mapping is handled on the daemon side. */
@@ -126,76 +200,90 @@
         packageInfo.uidType = appId >= Process.FIRST_APPLICATION_UID ? UidType.APPLICATION :
                 UidType.NATIVE;
 
-        if (packageName.startsWith("shared:")) {
-            String[] sharedUidPackages = mPackageManager.getPackagesForUid(uid);
-            if (sharedUidPackages == null) {
-                return packageInfo;
-            }
-            boolean seenVendor = false;
-            boolean seenSystem = false;
-            boolean seenThirdParty = false;
-            /*
-             * A shared UID has multiple packages associated with it and these packages may be
-             * mapped to different component types. Thus map the shared UID to the most restrictive
-             * component type.
-             */
-            for (int i = 0; i < sharedUidPackages.length; ++i) {
-                int componentType = getPackageComponentType(userId, sharedUidPackages[i]);
-                switch(componentType) {
-                    case ComponentType.VENDOR:
-                        seenVendor = true;
-                        break;
-                    case ComponentType.SYSTEM:
-                        seenSystem = true;
-                        break;
-                    case ComponentType.THIRD_PARTY:
-                        seenThirdParty = true;
-                        break;
-                    default:
-                        Slogf.w(TAG, "Unknown component type %d for package '%s'", componentType,
-                                sharedUidPackages[i]);
+        if (genericPackageName.startsWith(SHARED_PACKAGE_PREFIX)) {
+            List<String> packages = null;
+            synchronized (mLock) {
+                packages = mPackagesBySharedUid.get(uid);
+                if (packages == null) {
+                    return packageInfo;
                 }
             }
-            packageInfo.sharedUidPackages = Arrays.asList(sharedUidPackages);
-            if (seenVendor) {
-                packageInfo.componentType = ComponentType.VENDOR;
-            } else if (seenSystem) {
-                packageInfo.componentType = ComponentType.SYSTEM;
-            } else if (seenThirdParty) {
-                packageInfo.componentType = ComponentType.THIRD_PARTY;
+            List<ApplicationInfo> applicationInfos = new ArrayList<>();
+            for (int i = 0; i < packages.size(); ++i) {
+                try {
+                    applicationInfos.add(mPackageManager.getApplicationInfoAsUser(packages.get(i),
+                            /* flags= */ 0, userId));
+                } catch (PackageManager.NameNotFoundException e) {
+                    Slogf.e(TAG, "Package '%s' not found for user %d: %s", packages.get(i), userId,
+                            e);
+                }
             }
+            packageInfo.componentType = getSharedComponentType(
+                    applicationInfos, genericPackageName);
+            packageInfo.sharedUidPackages = new ArrayList<>(packages);
         } else {
-            packageInfo.componentType = getPackageComponentType(
-                    userId, packageName);
+            packageInfo.componentType = getUserPackageComponentType(
+                    userId, genericPackageName);
         }
         return packageInfo;
     }
 
-    private int getPackageComponentType(int userId, String packageName) {
+    /**
+     * Returns the most restrictive component type shared by the given application infos.
+     *
+     * A shared UID has multiple packages associated with it and these packages may be
+     * mapped to different component types. Thus map the shared UID to the most restrictive
+     * component type.
+     */
+    public int getSharedComponentType(List<ApplicationInfo> applicationInfos,
+            String genericPackageName) {
+        SparseBooleanArray seenComponents = new SparseBooleanArray();
+        for (int i = 0; i < applicationInfos.size(); ++i) {
+            int type = getComponentType(applicationInfos.get(i));
+            seenComponents.put(type, true);
+        }
+        if (seenComponents.get(ComponentType.VENDOR)) {
+            return ComponentType.VENDOR;
+        } else if (seenComponents.get(ComponentType.SYSTEM)) {
+            synchronized (mLock) {
+                for (int i = 0; i < mVendorPackagePrefixes.size(); ++i) {
+                    if (genericPackageName.startsWith(mVendorPackagePrefixes.get(i))) {
+                        return ComponentType.VENDOR;
+                    }
+                }
+            }
+            return ComponentType.SYSTEM;
+        } else if (seenComponents.get(ComponentType.THIRD_PARTY)) {
+            return ComponentType.THIRD_PARTY;
+        }
+        return ComponentType.UNKNOWN;
+    }
+
+    private int getUserPackageComponentType(int userId, String packageName) {
         try {
             ApplicationInfo info = mPackageManager.getApplicationInfoAsUser(packageName,
                     /* flags= */ 0, userId);
-            return getComponentType(packageName, info);
+            return getComponentType(info);
         } catch (PackageManager.NameNotFoundException e) {
             Slogf.e(TAG, "Package '%s' not found for user %d: %s", packageName, userId, e);
         }
         return ComponentType.UNKNOWN;
     }
 
-    /** Returns the component type for the given package and its application info. */
-    public int getComponentType(String packageName, ApplicationInfo info) {
-        if ((info.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0
-                || (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0
-                || (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_ODM) != 0) {
+    /** Returns the component type for the given application info. */
+    public int getComponentType(ApplicationInfo applicationInfo) {
+        if ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0
+                || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0
+                || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_ODM) != 0) {
             return ComponentType.VENDOR;
         }
-        if ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0
-                || (info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
-                || (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRODUCT) != 0
-                || (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_SYSTEM_EXT) != 0) {
+        if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
+                || (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
+                || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRODUCT) != 0
+                || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_SYSTEM_EXT) != 0) {
             synchronized (mLock) {
-                for (String prefix : mVendorPackagePrefixes) {
-                    if (packageName.startsWith(prefix)) {
+                for (int i = 0; i < mVendorPackagePrefixes.size(); ++i) {
+                    if (applicationInfo.packageName.startsWith(mVendorPackagePrefixes.get(i))) {
                         return ComponentType.VENDOR;
                     }
                 }
@@ -204,4 +292,10 @@
         }
         return ComponentType.THIRD_PARTY;
     }
+
+    void setVendorPackagePrefixes(List<String> vendorPackagePrefixes) {
+        synchronized (mLock) {
+            mVendorPackagePrefixes = vendorPackagePrefixes;
+        }
+    }
 }
diff --git a/service/src/com/android/car/watchdog/TimeSourceInterface.java b/service/src/com/android/car/watchdog/TimeSourceInterface.java
new file mode 100644
index 0000000..1bd6508
--- /dev/null
+++ b/service/src/com/android/car/watchdog/TimeSourceInterface.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.watchdog;
+
+import java.time.Instant;
+
+/**
+ * Provider for the current value of "now" for users of {@code java.time}.
+ */
+public interface TimeSourceInterface {
+    /** Returns the current instant from the time source implementation. */
+    Instant now();
+}
diff --git a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
index 8d15e0f..eb7ca30 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.internal.util.function.pooled.PooledLambda.obtainMessage;
+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;
@@ -56,12 +64,14 @@
 import android.car.watchdog.ResourceOveruseStats;
 import android.car.watchdoglib.CarWatchdogDaemonHelper;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 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;
@@ -73,15 +83,19 @@
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 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;
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -97,18 +111,19 @@
     public static final String INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA = "MEDIA";
     public static final String INTERNAL_APPLICATION_CATEGORY_TYPE_UNKNOWN = "UNKNOWN";
 
+    static final long RESOURCE_OVERUSE_KILLING_DELAY_MILLS = 10_000;
+
     private static final long MAX_WAIT_TIME_MILLS = 3_000;
 
     private final Context mContext;
     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 Map<String, PackageResourceUsage> mUsageByUserPackage = new ArrayMap<>();
+    private final ArrayMap<String, PackageResourceUsage> mUsageByUserPackage = new ArrayMap<>();
     @GuardedBy("mLock")
     private final List<PackageResourceOveruseAction> mOveruseActionsByUserPackage =
             new ArrayList<>();
@@ -120,54 +135,52 @@
             mOveruseSystemListenerInfosByUid = new SparseArray<>();
     /* Set of safe-to-kill system and vendor packages. */
     @GuardedBy("mLock")
-    public final Set<String> mSafeToKillPackages = new ArraySet<>();
+    public final ArraySet<String> mSafeToKillSystemPackages = new ArraySet<>();
+    @GuardedBy("mLock")
+    public final ArraySet<String> mSafeToKillVendorPackages = new ArraySet<>();
     /* Default killable state for packages when not updated by the user. */
     @GuardedBy("mLock")
-    public final Set<String> mDefaultNotKillablePackages = new ArraySet<>();
+    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;
     }
 
     /** Initializes the handler. */
     public void init() {
-        /*
-         * TODO(b/183947162): Opt-in to receive package change broadcast and handle package enabled
-         *  state changes.
-         *
-         * TODO(b/185287136): Persist in-memory data:
-         *  1. Read the current day's I/O overuse stats from database and push them
-         *  to the daemon.
-         *  2. Fetch the safe-to-kill from daemon on initialization and update mSafeToKillPackages.
-         */
-        synchronized (mLock) {
-            checkAndHandleDateChangeLocked();
-        }
+        /* 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/185287136): Write daily usage to SQLite DB storage. */
-        if (DEBUG) {
-            Slogf.d(TAG, "WatchdogPerfHandler is released");
-        }
-    }
-
     /** Dumps its state. */
     public void dump(IndentingPrintWriter writer) {
         /*
@@ -177,15 +190,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();
@@ -207,20 +231,20 @@
         int callingUid = Binder.getCallingUid();
         int callingUserId = UserHandle.getUserId(callingUid);
         UserHandle callingUserHandle = UserHandle.of(callingUserId);
-        String callingPackageName =
-                mPackageInfoHandler.getPackageNamesForUids(new int[]{callingUid})
+        String genericPackageName =
+                mPackageInfoHandler.getNamesForUids(new int[]{callingUid})
                         .get(callingUid, null);
-        if (callingPackageName == null) {
+        if (genericPackageName == null) {
             Slogf.w(TAG, "Failed to fetch package info for uid %d", callingUid);
             return new ResourceOveruseStats.Builder("", callingUserHandle).build();
         }
         ResourceOveruseStats.Builder statsBuilder =
-                new ResourceOveruseStats.Builder(callingPackageName, callingUserHandle);
-        statsBuilder.setIoOveruseStats(getIoOveruseStats(callingUserId, callingPackageName,
-                /* minimumBytesWritten= */ 0, maxStatsPeriod));
+                new ResourceOveruseStats.Builder(genericPackageName, callingUserHandle);
+        statsBuilder.setIoOveruseStats(
+                getIoOveruseStatsForPeriod(callingUserId, genericPackageName, maxStatsPeriod));
         if (DEBUG) {
             Slogf.d(TAG, "Returning all resource overuse stats for calling uid %d [user %d and "
-                            + "package '%s']", callingUid, callingUserId, callingPackageName);
+                            + "package '%s']", callingUid, callingUserId, genericPackageName);
         }
         return statsBuilder.build();
     }
@@ -240,14 +264,17 @@
                 "Must provide resource I/O overuse flag");
         long minimumBytesWritten = getMinimumBytesWritten(minimumStatsFlag);
         List<ResourceOveruseStats> allStats = new ArrayList<>();
-        for (PackageResourceUsage usage : mUsageByUserPackage.values()) {
-            ResourceOveruseStats.Builder statsBuilder = usage.getResourceOveruseStatsBuilder();
-            IoOveruseStats ioOveruseStats = getIoOveruseStats(usage.userId, usage.packageName,
-                    minimumBytesWritten, maxStatsPeriod);
-            if (ioOveruseStats == null) {
-                continue;
+        synchronized (mLock) {
+            for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+                PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+                ResourceOveruseStats.Builder statsBuilder = usage.getResourceOveruseStatsBuilder();
+                IoOveruseStats ioOveruseStats =
+                        getIoOveruseStatsLocked(usage, minimumBytesWritten, maxStatsPeriod);
+                if (ioOveruseStats == null) {
+                    continue;
+                }
+                allStats.add(statsBuilder.setIoOveruseStats(ioOveruseStats).build());
             }
-            allStats.add(statsBuilder.setIoOveruseStats(ioOveruseStats).build());
         }
         if (DEBUG) {
             Slogf.d(TAG, "Returning all resource overuse stats");
@@ -263,7 +290,7 @@
             @CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
         Objects.requireNonNull(packageName, "Package name must be non-null");
         Objects.requireNonNull(userHandle, "User handle must be non-null");
-        Preconditions.checkArgument((userHandle != UserHandle.ALL),
+        Preconditions.checkArgument(!userHandle.equals(UserHandle.ALL),
                 "Must provide the user handle for a specific user");
         Preconditions.checkArgument((resourceOveruseFlag > 0),
                 "Must provide valid resource overuse flag");
@@ -272,13 +299,19 @@
         // When more resource types are added, make this as optional.
         Preconditions.checkArgument((resourceOveruseFlag & FLAG_RESOURCE_OVERUSE_IO) != 0,
                 "Must provide resource I/O overuse flag");
+        String genericPackageName =
+                mPackageInfoHandler.getNameForUserPackage(packageName, userHandle.getIdentifier());
+        if (genericPackageName == null) {
+            throw new IllegalArgumentException("Package '" + packageName + "' not found");
+        }
         ResourceOveruseStats.Builder statsBuilder =
-                new ResourceOveruseStats.Builder(packageName, userHandle);
-        statsBuilder.setIoOveruseStats(getIoOveruseStats(userHandle.getIdentifier(), packageName,
-                /* minimumBytesWritten= */ 0, maxStatsPeriod));
+                new ResourceOveruseStats.Builder(genericPackageName, userHandle);
+        statsBuilder.setIoOveruseStats(getIoOveruseStatsForPeriod(userHandle.getIdentifier(),
+                genericPackageName, maxStatsPeriod));
         if (DEBUG) {
-            Slogf.d(TAG, "Returning resource overuse stats for user %d, package '%s'",
-                    userHandle.getIdentifier(), packageName);
+            Slogf.d(TAG, "Returning resource overuse stats for user %d, package '%s', "
+                    + "generic package '%s'", userHandle.getIdentifier(), packageName,
+                    genericPackageName);
         }
         return statsBuilder.build();
     }
@@ -330,31 +363,17 @@
             boolean isKillable) {
         Objects.requireNonNull(packageName, "Package name must be non-null");
         Objects.requireNonNull(userHandle, "User handle must be non-null");
-        if (userHandle == UserHandle.ALL) {
-            synchronized (mLock) {
-                for (PackageResourceUsage usage : mUsageByUserPackage.values()) {
-                    if (!usage.packageName.equals(packageName)) {
-                        continue;
-                    }
-                    if (!usage.setKillableState(isKillable)) {
-                        Slogf.e(TAG, "Cannot set killable state for package '%s'", packageName);
-                        throw new IllegalArgumentException(
-                                "Package killable state is not updatable");
-                    }
-                }
-                if (!isKillable) {
-                    mDefaultNotKillablePackages.add(packageName);
-                } else {
-                    mDefaultNotKillablePackages.remove(packageName);
-                }
-            }
-            if (DEBUG) {
-                Slogf.d(TAG, "Successfully set killable package state for all users");
-            }
+
+        if (userHandle.equals(UserHandle.ALL)) {
+            setPackageKillableStateForAllUsers(packageName, isKillable);
             return;
         }
         int userId = userHandle.getIdentifier();
-        String key = getUserPackageUniqueId(userId, packageName);
+        String genericPackageName = mPackageInfoHandler.getNameForUserPackage(packageName, userId);
+        if (genericPackageName == null) {
+            throw new IllegalArgumentException("Package '" + packageName + "' not found");
+        }
+        String key = getUserPackageUniqueId(userId, genericPackageName);
         synchronized (mLock) {
             /*
              * When the queried package is not cached in {@link mUsageByUserPackage}, the set API
@@ -366,11 +385,13 @@
              * state when pushing the latest stats. Ergo, the invalid killable state doesn't have
              * any effect.
              */
-            PackageResourceUsage usage = mUsageByUserPackage.getOrDefault(key,
-                    new PackageResourceUsage(userId, packageName));
-            if (!usage.setKillableState(isKillable)) {
+            PackageResourceUsage usage = mUsageByUserPackage.get(key);
+            if (usage == null) {
+                usage = new PackageResourceUsage(userId, genericPackageName);
+            }
+            if (!usage.verifyAndSetKillableStateLocked(isKillable)) {
                 Slogf.e(TAG, "User %d cannot set killable state for package '%s'",
-                        userHandle.getIdentifier(), packageName);
+                        userHandle.getIdentifier(), genericPackageName);
                 throw new IllegalArgumentException("Package killable state is not updatable");
             }
             mUsageByUserPackage.put(key, usage);
@@ -380,12 +401,48 @@
         }
     }
 
+    private void setPackageKillableStateForAllUsers(String packageName, boolean isKillable) {
+        UserManager userManager = UserManager.get(mContext);
+        List<UserInfo> userInfos = userManager.getAliveUsers();
+        String genericPackageName = null;
+        synchronized (mLock) {
+            for (int i = 0; i < userInfos.size(); ++i) {
+                int userId = userInfos.get(i).id;
+                String name = mPackageInfoHandler.getNameForUserPackage(packageName, userId);
+                if (name == null) {
+                    continue;
+                }
+                genericPackageName = name;
+                String key = getUserPackageUniqueId(userId, genericPackageName);
+                PackageResourceUsage usage = mUsageByUserPackage.get(key);
+                if (usage == null) {
+                    continue;
+                }
+                if (!usage.verifyAndSetKillableStateLocked(isKillable)) {
+                    Slogf.e(TAG, "Cannot set killable state for package '%s'", packageName);
+                    throw new IllegalArgumentException(
+                            "Package killable state is not updatable");
+                }
+            }
+            if (genericPackageName != null) {
+                if (!isKillable) {
+                    mDefaultNotKillableGenericPackages.add(genericPackageName);
+                } else {
+                    mDefaultNotKillableGenericPackages.remove(genericPackageName);
+                }
+            }
+        }
+        if (DEBUG) {
+            Slogf.d(TAG, "Successfully set killable package state for all users");
+        }
+    }
+
     /** Returns the list of package killable states on resource overuse for the user. */
     @NonNull
     public List<PackageKillableState> getPackageKillableStatesAsUser(UserHandle userHandle) {
         Objects.requireNonNull(userHandle, "User handle must be non-null");
         PackageManager pm = mContext.getPackageManager();
-        if (userHandle != UserHandle.ALL) {
+        if (!userHandle.equals(UserHandle.ALL)) {
             if (DEBUG) {
                 Slogf.d(TAG, "Returning all package killable states for user %d",
                         userHandle.getIdentifier());
@@ -395,8 +452,9 @@
         List<PackageKillableState> packageKillableStates = new ArrayList<>();
         UserManager userManager = UserManager.get(mContext);
         List<UserInfo> userInfos = userManager.getAliveUsers();
-        for (UserInfo userInfo : userInfos) {
-            packageKillableStates.addAll(getPackageKillableStatesForUserId(userInfo.id, pm));
+        for (int i = 0; i < userInfos.size(); ++i) {
+            packageKillableStates.addAll(
+                    getPackageKillableStatesForUserId(userInfos.get(i).id, pm));
         }
         if (DEBUG) {
             Slogf.d(TAG, "Returning all package killable states for all users");
@@ -406,24 +464,53 @@
 
     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 =
+                    new ArrayMap<>();
             for (int i = 0; i < packageInfos.size(); ++i) {
                 PackageInfo packageInfo = packageInfos.get(i);
-                String key = getUserPackageUniqueId(userId, packageInfo.packageName);
-                PackageResourceUsage usage = mUsageByUserPackage.getOrDefault(key,
-                        new PackageResourceUsage(userId, packageInfo.packageName));
-                int killableState = usage.syncAndFetchKillableStateLocked(
-                        mPackageInfoHandler.getComponentType(packageInfo.packageName,
-                                packageInfo.applicationInfo));
-                mUsageByUserPackage.put(key, usage);
-                states.add(
-                        new PackageKillableState(packageInfo.packageName, userId, killableState));
+                String genericPackageName = mPackageInfoHandler.getNameForPackage(packageInfo);
+                if (packageInfo.sharedUserId == null) {
+                    int componentType = mPackageInfoHandler.getComponentType(
+                            packageInfo.applicationInfo);
+                    int killableState = getPackageKillableStateForUserPackageLocked(
+                            userId, genericPackageName, componentType,
+                            isSafeToKillLocked(genericPackageName, componentType, null));
+                    states.add(new PackageKillableState(packageInfo.packageName, userId,
+                            killableState));
+                    continue;
+                }
+                List<ApplicationInfo> applicationInfos =
+                        applicationInfosBySharedPackage.get(genericPackageName);
+                if (applicationInfos == null) {
+                    applicationInfos = new ArrayList<>();
+                }
+                applicationInfos.add(packageInfo.applicationInfo);
+                applicationInfosBySharedPackage.put(genericPackageName, applicationInfos);
+            }
+            for (Map.Entry<String, List<ApplicationInfo>> entry :
+                    applicationInfosBySharedPackage.entrySet()) {
+                String genericPackageName = entry.getKey();
+                List<ApplicationInfo> applicationInfos = entry.getValue();
+                int componentType = mPackageInfoHandler.getSharedComponentType(
+                        applicationInfos, genericPackageName);
+                List<String> packageNames = new ArrayList<>(applicationInfos.size());
+                for (int i = 0; i < applicationInfos.size(); ++i) {
+                    packageNames.add(applicationInfos.get(i).packageName);
+                }
+                int killableState = getPackageKillableStateForUserPackageLocked(
+                        userId, genericPackageName, componentType,
+                        isSafeToKillLocked(genericPackageName, componentType, packageNames));
+                for (int i = 0; i < applicationInfos.size(); ++i) {
+                    states.add(new PackageKillableState(
+                            applicationInfos.get(i).packageName, userId, killableState));
+                }
             }
         }
         if (DEBUG) {
-            Slogf.d(TAG, "Returning the package killable states for a user package");
+            Slogf.d(TAG, "Returning the package killable states for user packages");
         }
         return states;
     }
@@ -439,28 +526,11 @@
                 "Must provide at least one configuration");
         Preconditions.checkArgument((resourceOveruseFlag > 0),
                 "Must provide valid resource overuse flag");
-        Set<Integer> seenComponentTypes = new ArraySet<>();
+        checkResourceOveruseConfigs(configurations, resourceOveruseFlag);
         List<android.automotive.watchdog.internal.ResourceOveruseConfiguration> internalConfigs =
                 new ArrayList<>();
-        for (ResourceOveruseConfiguration config : configurations) {
-            /*
-             * TODO(b/185287136): Make sure the validation done here matches the validation done in
-             *  the daemon so set requests retried at a later time will complete successfully.
-             */
-            int componentType = config.getComponentType();
-            if (toComponentTypeStr(componentType).equals("UNKNOWN")) {
-                throw new IllegalArgumentException("Invalid component type in the configuration");
-            }
-            if (seenComponentTypes.contains(componentType)) {
-                throw new IllegalArgumentException(
-                        "Cannot provide duplicate configurations for the same component type");
-            }
-            if ((resourceOveruseFlag & FLAG_RESOURCE_OVERUSE_IO) != 0
-                    && config.getIoOveruseConfiguration() == null) {
-                throw new IllegalArgumentException("Must provide I/O overuse configuration");
-            }
-            seenComponentTypes.add(config.getComponentType());
-            internalConfigs.add(toInternalResourceOveruseConfiguration(config,
+        for (int i = 0; i < configurations.size(); ++i) {
+            internalConfigs.add(toInternalResourceOveruseConfiguration(configurations.get(i),
                     resourceOveruseFlag));
         }
         synchronized (mLock) {
@@ -497,9 +567,9 @@
             throw new IllegalStateException(e);
         }
         List<ResourceOveruseConfiguration> configs = new ArrayList<>();
-        for (android.automotive.watchdog.internal.ResourceOveruseConfiguration internalConfig
-                : internalConfigs) {
-            configs.add(toResourceOveruseConfiguration(internalConfig, resourceOveruseFlag));
+        for (int i = 0; i < internalConfigs.size(); ++i) {
+            configs.add(
+                    toResourceOveruseConfiguration(internalConfigs.get(i), resourceOveruseFlag));
         }
         if (DEBUG) {
             Slogf.d(TAG, "Returning the resource overuse configuration");
@@ -509,20 +579,22 @@
 
     /** Processes the latest I/O overuse stats */
     public void latestIoOveruseStats(List<PackageIoOveruseStats> packageIoOveruseStats) {
+        SparseBooleanArray recurringIoOverusesByUid = new SparseBooleanArray();
         int[] uids = new int[packageIoOveruseStats.size()];
         for (int i = 0; i < packageIoOveruseStats.size(); ++i) {
             uids[i] = packageIoOveruseStats.get(i).uid;
         }
-        SparseArray<String> packageNamesByUid = mPackageInfoHandler.getPackageNamesForUids(uids);
+        SparseArray<String> genericPackageNamesByUid = mPackageInfoHandler.getNamesForUids(uids);
         synchronized (mLock) {
             checkAndHandleDateChangeLocked();
-            for (PackageIoOveruseStats stats : packageIoOveruseStats) {
-                String packageName = packageNamesByUid.get(stats.uid);
-                if (packageName == null) {
+            for (int i = 0; i < packageIoOveruseStats.size(); ++i) {
+                PackageIoOveruseStats stats = packageIoOveruseStats.get(i);
+                String genericPackageName = genericPackageNamesByUid.get(stats.uid);
+                if (genericPackageName == null) {
                     continue;
                 }
                 int userId = UserHandle.getUserId(stats.uid);
-                PackageResourceUsage usage = cacheAndFetchUsageLocked(userId, packageName,
+                PackageResourceUsage usage = cacheAndFetchUsageLocked(userId, genericPackageName,
                         stats.ioOveruseStats);
                 if (stats.shouldNotify) {
                     /*
@@ -532,7 +604,7 @@
                      */
                     ResourceOveruseStats resourceOveruseStats =
                             usage.getResourceOveruseStatsBuilder().setIoOveruseStats(
-                                    usage.getIoOveruseStats()).build();
+                                    usage.getIoOveruseStatsLocked()).build();
                     notifyResourceOveruseStatsLocked(stats.uid, resourceOveruseStats);
                 }
 
@@ -541,7 +613,7 @@
                 }
                 PackageResourceOveruseAction overuseAction = new PackageResourceOveruseAction();
                 overuseAction.packageIdentifier = new PackageIdentifier();
-                overuseAction.packageIdentifier.name = packageName;
+                overuseAction.packageIdentifier.name = genericPackageName;
                 overuseAction.packageIdentifier.uid = stats.uid;
                 overuseAction.resourceTypes = new int[]{ ResourceType.IO };
                 overuseAction.resourceOveruseActionType = NOT_KILLED;
@@ -551,7 +623,7 @@
                  * #2 The package has no recurring overuse behavior and the user opted to not
                  *    kill the package so honor the user's decision.
                  */
-                int killableState = usage.getKillableState();
+                int killableState = usage.getKillableStateLocked();
                 if (killableState == KILLABLE_STATE_NEVER) {
                     mOveruseActionsByUserPackage.add(overuseAction);
                     continue;
@@ -562,39 +634,16 @@
                     mOveruseActionsByUserPackage.add(overuseAction);
                     continue;
                 }
-                try {
-                    int oldEnabledState = -1;
-                    IPackageManager packageManager = ActivityThread.getPackageManager();
-                    if (!hasRecurringOveruse) {
-                        oldEnabledState = packageManager.getApplicationEnabledSetting(packageName,
-                                userId);
-
-                        if (oldEnabledState == COMPONENT_ENABLED_STATE_DISABLED
-                                || oldEnabledState == COMPONENT_ENABLED_STATE_DISABLED_USER
-                                || oldEnabledState == COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
-                            mOveruseActionsByUserPackage.add(overuseAction);
-                            continue;
-                        }
-                    }
-
-                    packageManager.setApplicationEnabledSetting(packageName,
-                            COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, /* flags= */ 0, userId,
-                            mContext.getPackageName());
-
-                    overuseAction.resourceOveruseActionType = hasRecurringOveruse
-                            ? KILLED_RECURRING_OVERUSE : KILLED;
-                    if (!hasRecurringOveruse) {
-                        usage.oldEnabledState = oldEnabledState;
-                    }
-                } catch (RemoteException e) {
-                    Slogf.e(TAG, "Failed to disable application enabled setting for user %d, "
-                            + "package '%s'", userId, packageName);
-                }
-                mOveruseActionsByUserPackage.add(overuseAction);
+                recurringIoOverusesByUid.put(stats.uid, hasRecurringOveruse);
             }
             if (!mOveruseActionsByUserPackage.isEmpty()) {
-                mMainHandler.sendMessage(obtainMessage(
-                        WatchdogPerfHandler::notifyActionsTakenOnOveruse, this));
+                mMainHandler.post(this::notifyActionsTakenOnOveruse);
+            }
+            if (recurringIoOverusesByUid.size() > 0) {
+                mMainHandler.postDelayed(
+                        () -> handleIoOveruseKilling(
+                                recurringIoOverusesByUid, genericPackageNamesByUid),
+                        mResourceOveruseKillingDelayMills);
             }
         }
         if (DEBUG) {
@@ -623,37 +672,262 @@
         }
     }
 
-    /** Resets the resource overuse stats for the given package. */
-    public void resetResourceOveruseStats(Set<String> packageNames) {
+    /** Handle packages that exceed resource overuse thresholds */
+    private void handleIoOveruseKilling(SparseBooleanArray recurringIoOverusesByUid,
+            SparseArray<String> genericPackageNamesByUid) {
+        IPackageManager packageManager = ActivityThread.getPackageManager();
         synchronized (mLock) {
-            for (PackageResourceUsage usage : mUsageByUserPackage.values()) {
-                if (packageNames.contains(usage.packageName)) {
-                    usage.resetStats();
-                    /*
-                     * TODO(b/185287136): When the stats are persisted in local DB, reset the stats
-                     *  for this package from local DB.
-                     */
+            for (int i = 0; i < recurringIoOverusesByUid.size(); i++) {
+                int uid = recurringIoOverusesByUid.keyAt(i);
+                boolean hasRecurringOveruse = recurringIoOverusesByUid.valueAt(i);
+                String genericPackageName = genericPackageNamesByUid.get(uid);
+                int userId = UserHandle.getUserId(uid);
+
+                PackageResourceOveruseAction overuseAction = new PackageResourceOveruseAction();
+                overuseAction.packageIdentifier = new PackageIdentifier();
+                overuseAction.packageIdentifier.name = genericPackageName;
+                overuseAction.packageIdentifier.uid = uid;
+                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);
+                }
+                for (int pkgIdx = 0; pkgIdx < packages.size(); pkgIdx++) {
+                    String packageName = packages.get(pkgIdx);
+                    try {
+                        if (!hasRecurringOveruse) {
+                            int currentEnabledState = packageManager.getApplicationEnabledSetting(
+                                    packageName, userId);
+                            if (currentEnabledState == COMPONENT_ENABLED_STATE_DISABLED
+                                    || currentEnabledState == COMPONENT_ENABLED_STATE_DISABLED_USER
+                                    || currentEnabledState
+                                    == COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
+                                continue;
+                            }
+                        }
+                        packageManager.setApplicationEnabledSetting(packageName,
+                                COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, /* flags= */ 0, userId,
+                                mContext.getPackageName());
+                        overuseAction.resourceOveruseActionType = hasRecurringOveruse
+                                ? KILLED_RECURRING_OVERUSE : KILLED;
+                    } catch (RemoteException e) {
+                        Slogf.e(TAG, "Failed to disable application for user %d, package '%s'",
+                                userId, packageName);
+                    }
+                }
+                if (overuseAction.resourceOveruseActionType == KILLED
+                        || overuseAction.resourceOveruseActionType == KILLED_RECURRING_OVERUSE) {
+                    usage.ioUsage.killed();
+                }
+                mOveruseActionsByUserPackage.add(overuseAction);
+            }
+            if (!mOveruseActionsByUserPackage.isEmpty()) {
+                notifyActionsTakenOnOveruse();
+            }
+        }
+    }
+
+    /** Resets the resource overuse settings and stats for the given generic package names. */
+    public void resetResourceOveruseStats(Set<String> genericPackageNames) {
+        synchronized (mLock) {
+            for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+                PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+                if (genericPackageNames.contains(usage.genericPackageName)) {
+                    usage.resetStatsLocked();
+                    usage.verifyAndSetKillableStateLocked(/* isKillable= */ true);
+                    mWatchdogStorage.deleteUserPackage(usage.userId, usage.genericPackageName);
                 }
             }
         }
     }
 
+    /** 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;
+        }
+    }
+
+    /** Fetches and syncs the resource overuse configurations from watchdog daemon. */
+    private void fetchAndSyncResourceOveruseConfigurations() {
+        synchronized (mLock) {
+            List<android.automotive.watchdog.internal.ResourceOveruseConfiguration> internalConfigs;
+            try {
+                internalConfigs = mCarWatchdogDaemonHelper.getResourceOveruseConfigurations();
+            } catch (RemoteException | RuntimeException e) {
+                Slogf.w(TAG, e, "Failed to fetch resource overuse configurations");
+                return;
+            }
+            if (internalConfigs.isEmpty()) {
+                Slogf.e(TAG, "Fetched resource overuse configurations are empty");
+                return;
+            }
+            mSafeToKillSystemPackages.clear();
+            mSafeToKillVendorPackages.clear();
+            for (int i = 0; i < internalConfigs.size(); i++) {
+                switch (internalConfigs.get(i).componentType) {
+                    case ComponentType.SYSTEM:
+                        mSafeToKillSystemPackages.addAll(internalConfigs.get(i).safeToKillPackages);
+                        break;
+                    case ComponentType.VENDOR:
+                        mSafeToKillVendorPackages.addAll(internalConfigs.get(i).safeToKillPackages);
+                        mPackageInfoHandler.setVendorPackagePrefixes(
+                                internalConfigs.get(i).vendorPackagePrefixes);
+                        break;
+                    default:
+                        // All third-party apps are killable.
+                        break;
+                }
+            }
+            if (DEBUG) {
+                Slogf.d(TAG, "Fetched and synced resource overuse configs.");
+            }
+        }
+    }
+
+    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) {
+        String key = getUserPackageUniqueId(userId, genericPackageName);
+        PackageResourceUsage usage = mUsageByUserPackage.get(key);
+        if (usage == null) {
+            usage = new PackageResourceUsage(userId, genericPackageName);
+        }
+        int killableState = usage.syncAndFetchKillableStateLocked(componentType, isSafeToKill);
+        mUsageByUserPackage.put(key, usage);
+        return killableState;
+    }
+
+    @GuardedBy("mLock")
     private void notifyResourceOveruseStatsLocked(int uid,
             ResourceOveruseStats resourceOveruseStats) {
-        String packageName = resourceOveruseStats.getPackageName();
+        String genericPackageName = resourceOveruseStats.getPackageName();
         ArrayList<ResourceOveruseListenerInfo> listenerInfos = mOveruseListenerInfosByUid.get(uid);
         if (listenerInfos != null) {
-            for (ResourceOveruseListenerInfo listenerInfo : listenerInfos) {
-                listenerInfo.notifyListener(FLAG_RESOURCE_OVERUSE_IO, uid, packageName,
-                        resourceOveruseStats);
+            for (int i = 0; i < listenerInfos.size(); ++i) {
+                listenerInfos.get(i).notifyListener(
+                        FLAG_RESOURCE_OVERUSE_IO, uid, genericPackageName, resourceOveruseStats);
             }
         }
         for (int i = 0; i < mOveruseSystemListenerInfosByUid.size(); ++i) {
             ArrayList<ResourceOveruseListenerInfo> systemListenerInfos =
                     mOveruseSystemListenerInfosByUid.valueAt(i);
-            for (ResourceOveruseListenerInfo listenerInfo : systemListenerInfos) {
-                listenerInfo.notifyListener(FLAG_RESOURCE_OVERUSE_IO, uid, packageName,
-                        resourceOveruseStats);
+            for (int j = 0; j < systemListenerInfos.size(); ++j) {
+                systemListenerInfos.get(j).notifyListener(
+                        FLAG_RESOURCE_OVERUSE_IO, uid, genericPackageName, resourceOveruseStats);
             }
         }
         if (DEBUG) {
@@ -663,43 +937,36 @@
 
     @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;
         }
-        for (PackageResourceUsage usage : mUsageByUserPackage.values()) {
-            if (usage.oldEnabledState > 0) {
-                // Forgive the daily disabled package on date change.
-                try {
-                    IPackageManager packageManager = ActivityThread.getPackageManager();
-                    if (packageManager.getApplicationEnabledSetting(usage.packageName,
-                            usage.userId)
-                            != COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
-                        continue;
-                    }
-                    packageManager.setApplicationEnabledSetting(usage.packageName,
-                            usage.oldEnabledState,
-                            /* flags= */ 0, usage.userId, mContext.getPackageName());
-                } catch (RemoteException e) {
-                    Slogf.e(TAG, "Failed to reset enabled setting for disabled package '%s', user "
-                            + "%d", usage.packageName, usage.userId);
-                }
-            }
-            /* TODO(b/170741935): Stash the old usage into SQLite DB storage. */
-            usage.resetStats();
+        /* After the first database read or on the first stats sync from the daemon, whichever
+         * happens first, the cached stats would either be empty or initialized from the database.
+         * In either case, don't write to database.
+         */
+        if (mLatestStatsReportDate != null && !mIsWrittenToDatabase) {
+            writeStatsLocked();
         }
+        for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+            mUsageByUserPackage.valueAt(i).resetStatsLocked();
+        }
+        mLatestStatsReportDate = currentDate;
         if (DEBUG) {
             Slogf.d(TAG, "Handled date change successfully");
         }
     }
 
-    private PackageResourceUsage cacheAndFetchUsageLocked(@UserIdInt int userId, String packageName,
+    @GuardedBy("mLock")
+    private PackageResourceUsage cacheAndFetchUsageLocked(
+            @UserIdInt int userId, String genericPackageName,
             android.automotive.watchdog.IoOveruseStats internalStats) {
-        String key = getUserPackageUniqueId(userId, packageName);
-        PackageResourceUsage usage = mUsageByUserPackage.getOrDefault(key,
-                new PackageResourceUsage(userId, packageName));
+        String key = getUserPackageUniqueId(userId, genericPackageName);
+        PackageResourceUsage usage = mUsageByUserPackage.get(key);
+        if (usage == null) {
+            usage = new PackageResourceUsage(userId, genericPackageName);
+        }
         usage.updateLocked(internalStats);
         mUsageByUserPackage.put(key, usage);
         return usage;
@@ -708,32 +975,64 @@
     @GuardedBy("mLock")
     private boolean isRecurringOveruseLocked(PackageResourceUsage ioUsage) {
         /*
-         * TODO(b/185287136): Look up I/O overuse history and determine whether or not the package
+         * TODO(b/192294393): Look up I/O overuse history and determine whether or not the package
          *  has recurring I/O overuse behavior.
          */
         return false;
     }
 
-    private IoOveruseStats getIoOveruseStats(int userId, String packageName,
+    private IoOveruseStats getIoOveruseStatsForPeriod(int userId, String genericPackageName,
+            @CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
+        synchronized (mLock) {
+            String key = getUserPackageUniqueId(userId, genericPackageName);
+            PackageResourceUsage usage = mUsageByUserPackage.get(key);
+            if (usage == null) {
+                return null;
+            }
+            return getIoOveruseStatsLocked(usage, /* minimumBytesWritten= */ 0, maxStatsPeriod);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private IoOveruseStats getIoOveruseStatsLocked(PackageResourceUsage usage,
             long minimumBytesWritten, @CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
-        String key = getUserPackageUniqueId(userId, packageName);
-        PackageResourceUsage usage = mUsageByUserPackage.get(key);
-        if (usage == null) {
+        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 stats = usage.getIoOveruseStats();
-        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.
-         */
+        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")
     private void addResourceOveruseListenerLocked(
             @CarWatchdogManager.ResourceOveruseFlag int resourceOveruseFlag,
             @NonNull IResourceOveruseListener listener,
@@ -750,8 +1049,8 @@
             listenerInfos = new ArrayList<>();
             listenerInfosByUid.put(callingUid, listenerInfos);
         }
-        for (ResourceOveruseListenerInfo listenerInfo : listenerInfos) {
-            if (listenerInfo.listener.asBinder() == binder) {
+        for (int i = 0; i < listenerInfos.size(); ++i) {
+            if (listenerInfos.get(i).listener.asBinder() == binder) {
                 throw new IllegalStateException(
                         "Cannot add " + listenerType + " as it is already added");
             }
@@ -772,6 +1071,7 @@
         }
     }
 
+    @GuardedBy("mLock")
     private void removeResourceOveruseListenerLocked(@NonNull IResourceOveruseListener listener,
             SparseArray<ArrayList<ResourceOveruseListenerInfo>> listenerInfosByUid) {
         int callingUid = Binder.getCallingUid();
@@ -784,9 +1084,9 @@
         }
         IBinder binder = listener.asBinder();
         ResourceOveruseListenerInfo cachedListenerInfo = null;
-        for (ResourceOveruseListenerInfo listenerInfo : listenerInfos) {
-            if (listenerInfo.listener.asBinder() == binder) {
-                cachedListenerInfo = listenerInfo;
+        for (int i = 0; i < listenerInfos.size(); ++i) {
+            if (listenerInfos.get(i).listener.asBinder() == binder) {
+                cachedListenerInfo = listenerInfos.get(i);
                 break;
             }
         }
@@ -844,6 +1144,7 @@
         boolean doClearPendingRequest = isPendingRequest;
         try {
             mCarWatchdogDaemonHelper.updateResourceOveruseConfigurations(configs);
+            mMainHandler.post(this::fetchAndSyncResourceOveruseConfigurations);
         } catch (RemoteException e) {
             if (e instanceof TransactionTooLargeException) {
                 throw e;
@@ -861,7 +1162,6 @@
                 }
             }
         }
-        /* TODO(b/185287136): Fetch safe-to-kill list from daemon and update mSafeToKillPackages. */
         if (DEBUG) {
             Slogf.d(TAG, "Set the resource overuse configuration successfully");
         }
@@ -887,20 +1187,59 @@
         }
     }
 
-    private static String getUserPackageUniqueId(int userId, String packageName) {
-        return String.valueOf(userId) + ":" + packageName;
+    @GuardedBy("mLock")
+    private boolean isSafeToKillLocked(String genericPackageName, int componentType,
+            List<String> sharedPackages) {
+        BiFunction<List<String>, Set<String>, Boolean> isSafeToKillAnyPackage =
+                (packages, safeToKillPackages) -> {
+                    if (packages == null) {
+                        return false;
+                    }
+                    for (int i = 0; i < packages.size(); i++) {
+                        if (safeToKillPackages.contains(packages.get(i))) {
+                            return true;
+                        }
+                    }
+                    return false;
+                };
+
+        switch (componentType) {
+            case ComponentType.SYSTEM:
+                if (mSafeToKillSystemPackages.contains(genericPackageName)) {
+                    return true;
+                }
+                return isSafeToKillAnyPackage.apply(sharedPackages, mSafeToKillSystemPackages);
+            case ComponentType.VENDOR:
+                if (mSafeToKillVendorPackages.contains(genericPackageName)) {
+                    return true;
+                }
+                /*
+                 * Packages under the vendor shared UID may contain system packages because when
+                 * CarWatchdogService derives the shared component type it attributes system
+                 * packages as vendor packages when there is at least one vendor package.
+                 */
+                return isSafeToKillAnyPackage.apply(sharedPackages, mSafeToKillSystemPackages)
+                        || isSafeToKillAnyPackage.apply(sharedPackages, mSafeToKillVendorPackages);
+            default:
+                // Third-party apps are always killable
+                return true;
+        }
+    }
+
+    private static String getUserPackageUniqueId(int userId, String genericPackageName) {
+        return String.valueOf(userId) + ":" + genericPackageName;
     }
 
     @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(
@@ -958,6 +1297,8 @@
                     metadata.appCategoryType = ApplicationCategoryType.MEDIA;
                     break;
                 default:
+                    Slogf.i(TAG, "Invalid application category type: %s skipping package: %s",
+                            entry.getValue(), metadata.packageName);
                     continue;
             }
             internalConfig.packageMetadata.add(metadata);
@@ -982,7 +1323,8 @@
                 config.getPackageSpecificThresholds());
         internalConfig.categorySpecificThresholds = toPerStateIoOveruseThresholds(
                 config.getAppCategorySpecificThresholds());
-        for (PerStateIoOveruseThreshold threshold : internalConfig.categorySpecificThresholds) {
+        for (int i = 0; i < internalConfig.categorySpecificThresholds.size(); ++i) {
+            PerStateIoOveruseThreshold threshold = internalConfig.categorySpecificThresholds.get(i);
             switch(threshold.name) {
                 case ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MAPS:
                     threshold.name = INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS;
@@ -1020,7 +1362,7 @@
             Map<String, PerStateBytes> thresholds) {
         List<PerStateIoOveruseThreshold> internalThresholds = new ArrayList<>();
         for (Map.Entry<String, PerStateBytes> entry : thresholds.entrySet()) {
-            if (!entry.getKey().isEmpty()) {
+            if (!thresholds.isEmpty()) {
                 internalThresholds.add(toPerStateIoOveruseThreshold(entry.getKey(),
                         entry.getValue()));
             }
@@ -1043,15 +1385,15 @@
             toInternalIoOveruseAlertThresholds(List<IoOveruseAlertThreshold> thresholds) {
         List<android.automotive.watchdog.internal.IoOveruseAlertThreshold> internalThresholds =
                 new ArrayList<>();
-        for (IoOveruseAlertThreshold threshold : thresholds) {
-            if (threshold.getDurationInSeconds() == 0
-                    || threshold.getWrittenBytesPerSecond() == 0) {
+        for (int i = 0; i < thresholds.size(); ++i) {
+            if (thresholds.get(i).getDurationInSeconds() == 0
+                    || thresholds.get(i).getWrittenBytesPerSecond() == 0) {
                 continue;
             }
             android.automotive.watchdog.internal.IoOveruseAlertThreshold internalThreshold =
                     new android.automotive.watchdog.internal.IoOveruseAlertThreshold();
-            internalThreshold.durationInSeconds = threshold.getDurationInSeconds();
-            internalThreshold.writtenBytesPerSecond = threshold.getWrittenBytesPerSecond();
+            internalThreshold.durationInSeconds = thresholds.get(i).getDurationInSeconds();
+            internalThreshold.writtenBytesPerSecond = thresholds.get(i).getWrittenBytesPerSecond();
             internalThresholds.add(internalThreshold);
         }
         return internalThresholds;
@@ -1060,10 +1402,10 @@
     private static ResourceOveruseConfiguration toResourceOveruseConfiguration(
             android.automotive.watchdog.internal.ResourceOveruseConfiguration internalConfig,
             @CarWatchdogManager.ResourceOveruseFlag int resourceOveruseFlag) {
-        Map<String, String> packagesToAppCategoryTypes = new ArrayMap<>();
-        for (PackageMetadata metadata : internalConfig.packageMetadata) {
+        ArrayMap<String, String> packagesToAppCategoryTypes = new ArrayMap<>();
+        for (int i = 0; i < internalConfig.packageMetadata.size(); ++i) {
             String categoryTypeStr;
-            switch (metadata.appCategoryType) {
+            switch (internalConfig.packageMetadata.get(i).appCategoryType) {
                 case ApplicationCategoryType.MAPS:
                     categoryTypeStr = ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MAPS;
                     break;
@@ -1073,7 +1415,8 @@
                 default:
                     continue;
             }
-            packagesToAppCategoryTypes.put(metadata.packageName, categoryTypeStr);
+            packagesToAppCategoryTypes.put(
+                    internalConfig.packageMetadata.get(i).packageName, categoryTypeStr);
         }
         ResourceOveruseConfiguration.Builder configBuilder =
                 new ResourceOveruseConfiguration.Builder(
@@ -1095,15 +1438,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);
-        Map<String, PerStateBytes> packageSpecificThresholds =
+        ArrayMap<String, PerStateBytes> packageSpecificThresholds =
                 toPerStateBytesMap(internalConfig.packageSpecificThresholds);
-        Map<String, PerStateBytes> appCategorySpecificThresholds =
+        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);
@@ -1114,11 +1465,12 @@
         return configBuilder.build();
     }
 
-    private static Map<String, PerStateBytes> toPerStateBytesMap(
+    private static ArrayMap<String, PerStateBytes> toPerStateBytesMap(
             List<PerStateIoOveruseThreshold> thresholds) {
-        Map<String, PerStateBytes> thresholdsMap = new ArrayMap<>();
-        for (PerStateIoOveruseThreshold threshold : thresholds) {
-            thresholdsMap.put(threshold.name, toPerStateBytes(threshold.perStateWriteBytes));
+        ArrayMap<String, PerStateBytes> thresholdsMap = new ArrayMap<>();
+        for (int i = 0; i < thresholds.size(); ++i) {
+            thresholdsMap.put(
+                    thresholds.get(i).name, toPerStateBytes(thresholds.get(i).perStateWriteBytes));
         }
         return thresholdsMap;
     }
@@ -1126,14 +1478,79 @@
     private static List<IoOveruseAlertThreshold> toIoOveruseAlertThresholds(
             List<android.automotive.watchdog.internal.IoOveruseAlertThreshold> internalThresholds) {
         List<IoOveruseAlertThreshold> thresholds = new ArrayList<>();
-        for (android.automotive.watchdog.internal.IoOveruseAlertThreshold internalThreshold
-                : internalThresholds) {
-            thresholds.add(new IoOveruseAlertThreshold(internalThreshold.durationInSeconds,
-                    internalThreshold.writtenBytesPerSecond));
+        for (int i = 0; i < internalThresholds.size(); ++i) {
+            thresholds.add(new IoOveruseAlertThreshold(internalThresholds.get(i).durationInSeconds,
+                    internalThresholds.get(i).writtenBytesPerSecond));
         }
         return thresholds;
     }
 
+    private static void checkResourceOveruseConfigs(
+            List<ResourceOveruseConfiguration> configurations,
+            @CarWatchdogManager.ResourceOveruseFlag int resourceOveruseFlag) {
+        ArraySet<Integer> seenComponentTypes = new ArraySet<>();
+        for (int i = 0; i < configurations.size(); ++i) {
+            ResourceOveruseConfiguration config = configurations.get(i);
+            if (seenComponentTypes.contains(config.getComponentType())) {
+                throw new IllegalArgumentException(
+                        "Cannot provide duplicate configurations for the same component type");
+            }
+            checkResourceOveruseConfig(config, resourceOveruseFlag);
+            seenComponentTypes.add(config.getComponentType());
+        }
+    }
+
+    private static void checkResourceOveruseConfig(ResourceOveruseConfiguration config,
+            @CarWatchdogManager.ResourceOveruseFlag int resourceOveruseFlag) {
+        int componentType = config.getComponentType();
+        if (toComponentTypeStr(componentType).equals("UNKNOWN")) {
+            throw new IllegalArgumentException(
+                    "Invalid component type in the configuration: " + componentType);
+        }
+        if ((resourceOveruseFlag & FLAG_RESOURCE_OVERUSE_IO) != 0
+                && config.getIoOveruseConfiguration() == null) {
+            throw new IllegalArgumentException("Must provide I/O overuse configuration");
+        }
+        checkIoOveruseConfig(config.getIoOveruseConfiguration(), componentType);
+    }
+
+    private static void checkIoOveruseConfig(IoOveruseConfiguration config, int componentType) {
+        if (config.getComponentLevelThresholds().getBackgroundModeBytes() <= 0
+                || config.getComponentLevelThresholds().getForegroundModeBytes() <= 0
+                || config.getComponentLevelThresholds().getGarageModeBytes() <= 0) {
+            throw new IllegalArgumentException(
+                    "For component: " + toComponentTypeStr(componentType)
+                            + " some thresholds are zero for: "
+                            + config.getComponentLevelThresholds().toString());
+        }
+        if (componentType == ComponentType.SYSTEM) {
+            List<IoOveruseAlertThreshold> systemThresholds = config.getSystemWideThresholds();
+            if (systemThresholds.isEmpty()) {
+                throw new IllegalArgumentException(
+                        "Empty system-wide alert thresholds provided in "
+                                + toComponentTypeStr(componentType)
+                                + " config.");
+            }
+            for (int i = 0; i < systemThresholds.size(); i++) {
+                checkIoOveruseAlertThreshold(systemThresholds.get(i));
+            }
+        }
+    }
+
+    private static void checkIoOveruseAlertThreshold(
+            IoOveruseAlertThreshold ioOveruseAlertThreshold) {
+        if (ioOveruseAlertThreshold.getDurationInSeconds() <= 0) {
+            throw new IllegalArgumentException(
+                    "System wide threshold duration must be greater than zero for: "
+                            + ioOveruseAlertThreshold);
+        }
+        if (ioOveruseAlertThreshold.getWrittenBytesPerSecond() <= 0) {
+            throw new IllegalArgumentException(
+                    "System wide threshold written bytes per second must be greater than zero for: "
+                            + ioOveruseAlertThreshold);
+        }
+    }
+
     private static void replaceKey(Map<String, PerStateBytes> map, String oldKey, String newKey) {
         PerStateBytes perStateBytes = map.get(oldKey);
         if (perStateBytes != null) {
@@ -1142,24 +1559,45 @@
         }
     }
 
-    private final class PackageResourceUsage {
-        public final String packageName;
-        public @UserIdInt final int userId;
-        public final PackageIoUsage ioUsage;
-        public int oldEnabledState;
+    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;
+        @GuardedBy("mLock")
+        public final PackageIoUsage ioUsage = new PackageIoUsage();
+        @GuardedBy("mLock")
         private @KillableState int mKillableState;
 
         /** Must be called only after acquiring {@link mLock} */
-        PackageResourceUsage(@UserIdInt int userId, String packageName) {
-            this.packageName = packageName;
+        PackageResourceUsage(@UserIdInt int userId, String genericPackageName) {
+            this.genericPackageName = genericPackageName;
             this.userId = userId;
-            this.ioUsage = new PackageIoUsage();
-            this.oldEnabledState = -1;
-            this.mKillableState = mDefaultNotKillablePackages.contains(packageName)
+            this.mKillableState = mDefaultNotKillableGenericPackages.contains(genericPackageName)
                     ? KILLABLE_STATE_NO : KILLABLE_STATE_YES;
         }
 
+        public boolean isSharedPackage() {
+            return this.genericPackageName.startsWith(SHARED_PACKAGE_PREFIX);
+        }
+
+        @GuardedBy("mLock")
         public void updateLocked(android.automotive.watchdog.IoOveruseStats internalStats) {
             if (!internalStats.killableOnOveruse) {
                 /*
@@ -1175,29 +1613,36 @@
                  * This case happens when a previously unsafe to kill system/vendor package was
                  * recently marked as safe-to-kill so update the old state to the default value.
                  */
-                mKillableState = mDefaultNotKillablePackages.contains(packageName)
+                mKillableState = mDefaultNotKillableGenericPackages.contains(genericPackageName)
                         ? KILLABLE_STATE_NO : KILLABLE_STATE_YES;
             }
             ioUsage.update(internalStats);
         }
 
         public ResourceOveruseStats.Builder getResourceOveruseStatsBuilder() {
-            return new ResourceOveruseStats.Builder(packageName, UserHandle.of(userId));
+            return new ResourceOveruseStats.Builder(genericPackageName, UserHandle.of(userId));
         }
 
-        public IoOveruseStats getIoOveruseStats() {
+        @GuardedBy("mLock")
+        public IoOveruseStats getIoOveruseStatsLocked() {
             if (!ioUsage.hasUsage()) {
                 return null;
             }
-            return ioUsage.getStatsBuilder().setKillableOnOveruse(
-                        mKillableState != KILLABLE_STATE_NEVER).build();
+            return ioUsage.getIoOveruseStats(mKillableState != KILLABLE_STATE_NEVER);
         }
 
-        public @KillableState int getKillableState() {
+        @GuardedBy("mLock")
+        public @KillableState int getKillableStateLocked() {
             return mKillableState;
         }
 
-        public boolean setKillableState(boolean isKillable) {
+        @GuardedBy("mLock")
+        public void setKillableStateLocked(@KillableState int killableState) {
+            mKillableState = killableState;
+        }
+
+        @GuardedBy("mLock")
+        public boolean verifyAndSetKillableStateLocked(boolean isKillable) {
             if (mKillableState == KILLABLE_STATE_NEVER) {
                 return false;
             }
@@ -1205,43 +1650,73 @@
             return true;
         }
 
-        public int syncAndFetchKillableStateLocked(int myComponentType) {
+        @GuardedBy("mLock")
+        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.
              */
-            if (myComponentType != ComponentType.THIRD_PARTY
-                    && !mSafeToKillPackages.contains(packageName)) {
+            if (myComponentType != ComponentType.THIRD_PARTY && !isSafeToKill) {
                 mKillableState = KILLABLE_STATE_NEVER;
             } else if (mKillableState == KILLABLE_STATE_NEVER) {
-                mKillableState = mDefaultNotKillablePackages.contains(packageName)
+                mKillableState = mDefaultNotKillableGenericPackages.contains(genericPackageName)
                         ? KILLABLE_STATE_NO : KILLABLE_STATE_YES;
             }
             return mKillableState;
         }
 
-        public void resetStats() {
+        @GuardedBy("mLock")
+        public void resetStatsLocked() {
             ioUsage.resetStats();
         }
     }
-
-    private static final class PackageIoUsage {
+    /** Defines I/O usage fields for a package. */
+    public static final class PackageIoUsage {
+        private static final android.automotive.watchdog.PerStateBytes DEFAULT_PER_STATE_BYTES =
+                new android.automotive.watchdog.PerStateBytes();
         private android.automotive.watchdog.IoOveruseStats mIoOveruseStats;
         private android.automotive.watchdog.PerStateBytes mForgivenWriteBytes;
-        private long mTotalTimesKilled;
+        private int mTotalTimesKilled;
 
-        PackageIoUsage() {
+        private PackageIoUsage() {
+            mForgivenWriteBytes = DEFAULT_PER_STATE_BYTES;
             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()) {
                 /*
@@ -1254,13 +1729,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;
             }
@@ -1270,9 +1743,13 @@
                     || remaining.garageModeBytes == 0;
         }
 
-        public void resetStats() {
+        void killed() {
+            ++mTotalTimesKilled;
+        }
+
+        void resetStats() {
             mIoOveruseStats = null;
-            mForgivenWriteBytes = null;
+            mForgivenWriteBytes = DEFAULT_PER_STATE_BYTES;
             mTotalTimesKilled = 0;
         }
     }
@@ -1319,7 +1796,7 @@
         }
 
         public void notifyListener(@CarWatchdogManager.ResourceOveruseFlag int resourceType,
-                int overusingUid, String overusingPackage,
+                int overusingUid, String overusingGenericPackageName,
                 ResourceOveruseStats resourceOveruseStats) {
             if ((flag & resourceType) == 0) {
                 return;
@@ -1328,9 +1805,9 @@
                 listener.onOveruse(resourceOveruseStats);
             } catch (RemoteException e) {
                 Slogf.e(TAG, "Failed to notify %s (uid %d, pid: %d) of resource overuse by "
-                                + "package(uid %d, package '%s'): %s",
+                                + "package(uid %d, generic package name '%s'): %s",
                         (isListenerForSystem ? "system listener" : "listener"), uid, pid,
-                        overusingUid, overusingPackage, e);
+                        overusingUid, overusingGenericPackageName, e);
             }
         }
 
diff --git a/service/src/com/android/car/watchdog/WatchdogProcessHandler.java b/service/src/com/android/car/watchdog/WatchdogProcessHandler.java
index cce3d15..63aa135 100644
--- a/service/src/com/android/car/watchdog/WatchdogProcessHandler.java
+++ b/service/src/com/android/car/watchdog/WatchdogProcessHandler.java
@@ -20,8 +20,6 @@
 import static android.car.watchdog.CarWatchdogManager.TIMEOUT_MODERATE;
 import static android.car.watchdog.CarWatchdogManager.TIMEOUT_NORMAL;
 
-import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.automotive.watchdog.internal.ICarWatchdogServiceForSystem;
@@ -218,8 +216,7 @@
 
     /** Posts health check message */
     public void postHealthCheckMessage(int sessionId) {
-        mMainHandler.sendMessage(obtainMessage(
-                WatchdogProcessHandler::doHealthCheck, this, sessionId));
+        mMainHandler.post(() -> doHealthCheck(sessionId));
     }
 
     /** Returns the registered and alive client count. */
@@ -240,6 +237,16 @@
         }
     }
 
+    /** Enables/disables the watchdog daemon client health check process. */
+    void controlProcessHealthCheck(boolean disable) {
+        try {
+            mCarWatchdogDaemonHelper.controlProcessHealthCheck(disable);
+        } catch (RemoteException e) {
+            Slogf.w(CarWatchdogService.TAG,
+                    "Cannot enable/disable the car watchdog daemon health check process: %s", e);
+        }
+    }
+
     private void onClientDeath(ICarWatchdogServiceCallback client, int timeout) {
         synchronized (mLock) {
             removeClientLocked(client.asBinder(), timeout);
@@ -312,8 +319,8 @@
             }
         }
         sendPingToClients(timeout);
-        mMainHandler.sendMessageDelayed(obtainMessage(WatchdogProcessHandler::analyzeClientResponse,
-                this, timeout), timeoutToDurationMs(timeout));
+        mMainHandler.postDelayed(
+                () -> analyzeClientResponse(timeout), timeoutToDurationMs(timeout));
     }
 
     private int getNewSessionId() {
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..78b6927
--- /dev/null
+++ b/service/src/com/android/car/watchdog/WatchdogStorage.java
@@ -0,0 +1,681 @@
+/*
+ * 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;
+    }
+
+    /** Deletes user package settings and resource overuse stats. */
+    public void deleteUserPackage(@UserIdInt int userId, String packageName) {
+        UserPackage userPackage = mUserPackagesByKey.get(UserPackage.getKey(userId, packageName));
+        if (userPackage == null) {
+            Slogf.w(TAG, "Failed to find unique database id for user id '%d' and package '%s",
+                    userId, packageName);
+            return;
+        }
+        mUserPackagesByKey.remove(userPackage.getKey());
+        mUserPackagesById.remove(userPackage.getUniqueId());
+        try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+            UserPackageSettingsTable.deleteUserPackage(db, userId, packageName);
+        }
+    }
+
+    /**
+     * 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;
+            }
+        }
+
+        public static void deleteUserPackage(SQLiteDatabase db, @UserIdInt int userId,
+                String packageName) {
+            String whereClause = COLUMN_USER_ID + "= ? and " + COLUMN_PACKAGE_NAME + "= ?";
+            String[] whereArgs = new String[]{String.valueOf(userId), packageName};
+            int deletedRows = db.delete(TABLE_NAME, whereClause, whereArgs);
+            Slogf.i(TAG, "Deleted %d user package settings db rows for user %d and package %s",
+                    deletedRows, userId, packageName);
+        }
+    }
+
+    /** 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/AdasLocationTestApp/Android.bp b/tests/AdasLocationTestApp/Android.bp
new file mode 100644
index 0000000..cc52ec7
--- /dev/null
+++ b/tests/AdasLocationTestApp/Android.bp
@@ -0,0 +1,51 @@
+// 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "AdasLocationTestApp",
+
+    srcs: ["src/**/*.java"],
+
+    resource_dirs: ["res"],
+
+    platform_apis: true,
+
+    optimize: {
+        enabled: false,
+    },
+
+    enforce_uses_libs: false,
+    dex_preopt: {
+        enabled: false,
+    },
+
+    required: ["allowed_privapp_com.google.android.car.adaslocation"],
+
+    privileged: true,
+
+    certificate: "platform",
+
+    static_libs: [
+            "com.google.android.material_material",
+            "androidx.appcompat_appcompat",
+    ],
+
+    libs: ["android.car"],
+}
\ No newline at end of file
diff --git a/tests/AdasLocationTestApp/AndroidManifest.xml b/tests/AdasLocationTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..6dcd183
--- /dev/null
+++ b/tests/AdasLocationTestApp/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<!--
+  ~ 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.adaslocation"
+          android:sharedUserId="android.uid.system">
+
+    <!-- The app needs to access device location to verify ADAS and main location switch work as
+    expected. -->
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+
+    <application android:label="AdasLocationTestApp">
+        <activity android:name=".AdasLocationActivity"
+                  android:theme="@style/Theme.AppCompat"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/tests/AdasLocationTestApp/res/layout/main_activity.xml b/tests/AdasLocationTestApp/res/layout/main_activity.xml
new file mode 100644
index 0000000..0173cd6
--- /dev/null
+++ b/tests/AdasLocationTestApp/res/layout/main_activity.xml
@@ -0,0 +1,102 @@
+<?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.
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                android:layout_width="fill_parent"
+                android:layout_height="match_parent"
+                android:orientation="vertical">
+    <TextView
+        android:id="@+id/main_location_enabled"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:layout_marginTop="100dp"
+        android:textStyle="bold"
+        android:text="main location enabled: "/>
+    <TextView
+        android:id="@+id/main_location_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_toRightOf="@id/main_location_enabled"
+        android:layout_marginTop="100dp"
+        android:textStyle="bold"/>
+    <TextView
+        android:id="@+id/adas_location_enabled"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:layout_below="@id/main_location_enabled"
+        android:textStyle="bold"
+        android:text="adas location enabled: "/>
+    <TextView
+        android:id="@+id/adas_location_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/main_location_status"
+        android:layout_toRightOf="@id/adas_location_enabled"
+        android:textStyle="bold"/>
+    <TextView
+        android:id="@+id/current_location"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/current_location"
+        android:layout_alignParentLeft="true"
+        android:layout_marginLeft="350dp"
+        android:layout_marginTop= "200dp"
+        android:textStyle="bold"/>
+    <TextView
+        android:id="@+id/current_location_result"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/current_location"
+        android:layout_alignParentLeft="true"
+        android:layout_marginLeft="350dp"
+        android:textStyle="bold"/>
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/observe_fab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/current_location_result"
+        android:layout_alignParentLeft="true"
+        app:useCompatPadding="true"
+        android:layout_marginLeft="350dp"/>
+    <TextView
+        android:id="@+id/last_location"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/last_location"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="350dp"
+        android:layout_marginTop= "200dp"
+        android:textStyle="bold"/>
+    <TextView
+        android:id="@+id/last_location_result"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/last_location"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="350dp"
+        android:textStyle="bold"/>
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/query_fab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/last_location_result"
+        android:layout_alignParentRight="true"
+        app:useCompatPadding="true"
+        android:layout_marginRight="350dp"/>
+</RelativeLayout>
diff --git a/tests/AdasLocationTestApp/res/values/strings.xml b/tests/AdasLocationTestApp/res/values/strings.xml
new file mode 100644
index 0000000..3b97648
--- /dev/null
+++ b/tests/AdasLocationTestApp/res/values/strings.xml
@@ -0,0 +1,21 @@
+<!--
+  ~ 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">
+    <string name="last_location" translatable="false">Last Location</string>
+    <string name="current_location" translatable="false">Current Location</string>
+    <string name="no_last_location" translatable="false">no last location</string>
+    <string name="waiting_for_location" translatable="false">waiting for location</string>
+</resources>
\ No newline at end of file
diff --git a/tests/AdasLocationTestApp/src/com/google/android/car/adaslocation/AdasLocationActivity.java b/tests/AdasLocationTestApp/src/com/google/android/car/adaslocation/AdasLocationActivity.java
new file mode 100644
index 0000000..bcb29f1
--- /dev/null
+++ b/tests/AdasLocationTestApp/src/com/google/android/car/adaslocation/AdasLocationActivity.java
@@ -0,0 +1,195 @@
+/*
+ * 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.google.android.car.adaslocation;
+
+import android.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public final class AdasLocationActivity extends AppCompatActivity {
+    private static final int KS_PERMISSIONS_REQUEST = 1;
+
+    private static final String[] REQUIRED_PERMISSIONS = new String[]{
+            Manifest.permission.ACCESS_FINE_LOCATION,
+            Manifest.permission.ACCESS_COARSE_LOCATION,
+    };
+
+    private boolean mIsRegister;
+    private LocationManager mLocationManager;
+    private FloatingActionButton mObserveFab;
+    private TextView mObserveLocationResult;
+    private LocationListener mLocationListener;
+    private FloatingActionButton mQueryFab;
+    private TextView mLastLocationResult;
+    private TextView mMainLocationEnabled;
+    private TextView mAdasLocationEnabled;
+    private BroadcastReceiver mReceiver;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main_activity);
+
+        mLocationManager = getApplicationContext().getSystemService(LocationManager.class);
+        mObserveFab = findViewById(R.id.observe_fab);
+        mObserveLocationResult = findViewById(R.id.current_location_result);
+        mLocationListener = new LocationListener() {
+            @Override
+            public void onLocationChanged(Location location) {
+                mObserveLocationResult.setText(locationToFormattedString(location));
+            }
+
+            @Override
+            public void onProviderEnabled(String provider) {
+            }
+
+            @Override
+            public void onProviderDisabled(String provider) {
+            }
+        };
+        mObserveFab.setOnClickListener(
+                v -> {
+                    if (!mIsRegister) {
+                        startListening();
+                    } else {
+                        stopListening();
+                    }
+                }
+        );
+        mQueryFab = findViewById(R.id.query_fab);
+        mLastLocationResult = findViewById(R.id.last_location_result);
+        mQueryFab.setOnClickListener(
+                v -> {
+                    Location location = mLocationManager
+                            .getLastKnownLocation(LocationManager.GPS_PROVIDER);
+                    if (location != null) {
+                        mLastLocationResult.setText(locationToFormattedString(location));
+                    } else {
+                        mLastLocationResult.setText(R.string.no_last_location);
+                    }
+                }
+        );
+
+        mReceiver =
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        if (LocationManager.MODE_CHANGED_ACTION == intent.getAction()) {
+                            mMainLocationEnabled.setText(Boolean.toString(mLocationManager
+                                    .isLocationEnabled()));
+                            return;
+                        }
+                        if (LocationManager.ACTION_ADAS_GNSS_ENABLED_CHANGED
+                                == intent.getAction()) {
+                            mAdasLocationEnabled
+                                    .setText(Boolean.toString(mLocationManager
+                                            .isAdasGnssLocationEnabled()));
+                            return;
+                        }
+                    }
+                };
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        initPermissions();
+
+        mMainLocationEnabled = findViewById(R.id.main_location_status);
+        mMainLocationEnabled.setText(Boolean.toString(mLocationManager.isLocationEnabled()));
+        mAdasLocationEnabled = findViewById(R.id.adas_location_status);
+        mAdasLocationEnabled.setText(Boolean.toString(mLocationManager
+                .isAdasGnssLocationEnabled()));
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION);
+        intentFilter.addAction(LocationManager.ACTION_ADAS_GNSS_ENABLED_CHANGED);
+        registerReceiver(mReceiver, intentFilter);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        Set<String> missingPermissions = checkExistingPermissions();
+        if (!missingPermissions.isEmpty()) {
+            return;
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        if (mIsRegister) {
+            stopListening();
+        }
+        mLastLocationResult.setText("");
+    }
+
+    private static String locationToFormattedString(Location location) {
+        return String.format("Location: lat=%10.6f, lon=%10.6f ",
+                location.getLatitude(),
+                location.getLongitude());
+    }
+
+    private void initPermissions() {
+        Set<String> missingPermissions = checkExistingPermissions();
+        if (!missingPermissions.isEmpty()) {
+            requestPermissions(missingPermissions);
+        }
+    }
+
+    private Set<String> checkExistingPermissions() {
+        return Arrays.stream(REQUIRED_PERMISSIONS).filter(permission -> ActivityCompat
+                .checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED)
+                .collect(Collectors.toSet());
+    }
+
+    private void requestPermissions(Set<String> permissions) {
+        requestPermissions(permissions.toArray(new String[permissions.size()]),
+                KS_PERMISSIONS_REQUEST);
+    }
+
+    private void startListening() {
+        mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
+                0, 0, mLocationListener);
+        mObserveLocationResult.setText(R.string.waiting_for_location);
+        mIsRegister = true;
+    }
+
+    private void stopListening() {
+        mLocationManager.removeUpdates(mLocationListener);
+        mObserveLocationResult.setText("");
+        mIsRegister = false;
+    }
+}
diff --git a/tests/CarEvsCameraPreviewApp/Android.bp b/tests/CarEvsCameraPreviewApp/Android.bp
index 99f0505..fea6aaf 100644
--- a/tests/CarEvsCameraPreviewApp/Android.bp
+++ b/tests/CarEvsCameraPreviewApp/Android.bp
@@ -26,8 +26,8 @@
 
     resource_dirs: ["res"],
 
-    // This app uses system APIs.
-    sdk_version: "system_current",
+    // registerReceiverForAllUsers() is a hidden api.
+    platform_apis: true,
 
     certificate: "platform",
 
diff --git a/tests/CarEvsCameraPreviewApp/AndroidManifest.xml b/tests/CarEvsCameraPreviewApp/AndroidManifest.xml
index 6e4c254..201c90b 100644
--- a/tests/CarEvsCameraPreviewApp/AndroidManifest.xml
+++ b/tests/CarEvsCameraPreviewApp/AndroidManifest.xml
@@ -24,6 +24,8 @@
     <uses-permission android:name="android.car.permission.MONITOR_CAR_EVS_STATUS" />
 
     <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
+    <!-- for registerReceiverForAllUsers() -->
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
 
     <application android:label="@string/app_name"
             android:icon="@drawable/rearview"
@@ -38,6 +40,8 @@
                 android:showForAllUsers="true"
                 android:theme="@style/Theme.Transparent"
                 android:turnScreenOn="true">
+            <meta-data android:name="distractionOptimized"
+                    android:value="true"/>
         </activity>
 
         <activity android:name=".CarEvsCameraActivity"
@@ -54,6 +58,8 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+            <meta-data android:name="distractionOptimized"
+                    android:value="true"/>
         </activity>
 
     </application>
diff --git a/tests/CarEvsCameraPreviewApp/OWNERS b/tests/CarEvsCameraPreviewApp/OWNERS
new file mode 100644
index 0000000..9b47ecd
--- /dev/null
+++ b/tests/CarEvsCameraPreviewApp/OWNERS
@@ -0,0 +1,4 @@
+# Project owners
+ankitarora@google.com
+changyeon@google.com
+ycheo@google.com
diff --git a/tests/CarEvsCameraPreviewApp/res/values/config.xml b/tests/CarEvsCameraPreviewApp/res/values/config.xml
new file mode 100644
index 0000000..85eb0b1
--- /dev/null
+++ b/tests/CarEvsCameraPreviewApp/res/values/config.xml
@@ -0,0 +1,23 @@
+<!--
+  ~ 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>
+
+    <!-- In-plane rotation angle of the rearview camera device in degree -->
+    <integer name="config_evsRearviewCameraInPlaneRotationAngle">0</integer>
+</resources>
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..cd6c840 100644
--- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
+++ b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
@@ -24,7 +24,10 @@
 import android.car.CarNotConnectedException;
 import android.car.evs.CarEvsBufferDescriptor;
 import android.car.evs.CarEvsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.graphics.PixelFormat;
 import android.hardware.display.DisplayManager;
 import android.os.Bundle;
@@ -34,8 +37,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 +58,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 */
@@ -145,11 +149,32 @@
         }
     };
 
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+                finish();
+            } else {
+                Log.e(TAG, "Unexpected intent " + intent);
+            }
+        }
+    };
+
+    // To close the PreviewActiivty when Home button is clicked.
+    private void registerBroadcastReceiver() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        // Need to register the receiver for all users, because we want to receive the Intent after
+        // the user is changed.
+        registerReceiverForAllUsers(mBroadcastReceiver, filter, null, null);
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         Log.d(TAG, "onCreate");
         super.onCreate(savedInstanceState);
 
+        registerBroadcastReceiver();
         parseExtra(getIntent());
 
         setShowWhenLocked(true);
@@ -164,8 +189,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 +199,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,8 +278,10 @@
         mDisplayManager.unregisterDisplayListener(mDisplayListener);
         if (mUseSystemWindow) {
             WindowManager wm = getSystemService(WindowManager.class);
-            wm.removeView(mPreviewContainer);
+            wm.removeView(mRootView);
         }
+
+        unregisterReceiver(mBroadcastReceiver);
     }
 
     private void handleVideoStreamLocked() {
diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java
index 6d8b1f4..f04d15b 100644
--- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java
+++ b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java
@@ -54,10 +54,10 @@
              1.0f, -1.0f, 0.0f };
 
     private static final float[] sVertCarTexData = {
-            0.0f, 0.0f,
-            1.0f, 0.0f,
-            0.0f, 1.0f,
-            1.0f, 1.0f };
+           -0.5f, -0.5f,
+            0.5f, -0.5f,
+           -0.5f,  0.5f,
+            0.5f,  0.5f };
 
     private static final float[] sIdentityMatrix = {
             1.0f, 0.0f, 0.0f, 0.0f,
@@ -105,9 +105,27 @@
         mVertCarPos = ByteBuffer.allocateDirect(sVertCarPosData.length * FLOAT_SIZE_BYTES)
                 .order(ByteOrder.nativeOrder()).asFloatBuffer();
         mVertCarPos.put(sVertCarPosData).position(0);
+
+        // Rotates the matrix in counter-clockwise
+        int angleInDegree = mContext.getResources().getInteger(
+                R.integer.config_evsRearviewCameraInPlaneRotationAngle);
+        double angleInRadian = Math.toRadians(angleInDegree);
+        float[] rotated = {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f};
+        float sin = (float)Math.sin(angleInRadian);
+        float cos = (float)Math.cos(angleInRadian);
+
+        rotated[0] += cos * sVertCarTexData[0] - sin * sVertCarTexData[1];
+        rotated[1] += sin * sVertCarTexData[0] + cos * sVertCarTexData[1];
+        rotated[2] += cos * sVertCarTexData[2] - sin * sVertCarTexData[3];
+        rotated[3] += sin * sVertCarTexData[2] + cos * sVertCarTexData[3];
+        rotated[4] += cos * sVertCarTexData[4] - sin * sVertCarTexData[5];
+        rotated[5] += sin * sVertCarTexData[4] + cos * sVertCarTexData[5];
+        rotated[6] += cos * sVertCarTexData[6] - sin * sVertCarTexData[7];
+        rotated[7] += sin * sVertCarTexData[6] + cos * sVertCarTexData[7];
+
         mVertCarTex = ByteBuffer.allocateDirect(sVertCarTexData.length * FLOAT_SIZE_BYTES)
                 .order(ByteOrder.nativeOrder()).asFloatBuffer();
-        mVertCarTex.put(sVertCarTexData).position(0);
+        mVertCarTex.put(rotated).position(0);
     }
 
     public void clearBuffer() {
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/Android.bp b/tests/EmbeddedKitchenSinkApp/Android.bp
index ef70cc9..ac942aa 100644
--- a/tests/EmbeddedKitchenSinkApp/Android.bp
+++ b/tests/EmbeddedKitchenSinkApp/Android.bp
@@ -46,12 +46,14 @@
         "androidx.appcompat_appcompat",
         "car-admin-ui-lib",
         "car-ui-lib",
+        "car-qc-lib",
         "android.hidl.base-V1.0-java",
         "android.hardware.automotive.vehicle-V2.0-java",
         "vehicle-hal-support-lib-for-test",
         "com.android.car.keventreader-client",
         "guava",
         "android.car.cluster.navigation",
+        "cartelemetry-protos",
         "car-experimental-api-static-lib",
     ],
 
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index 8f9f2d2..5c5029b 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -51,6 +51,8 @@
     <uses-permission android:name="android.car.permission.STORAGE_MONITORING"/>
     <uses-permission android:name="android.car.permission.CAR_DYNAMICS_STATE"/>
     <uses-permission android:name="android.car.permission.CONTROL_APP_BLOCKING"/>
+    <!-- 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/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml
new file mode 100644
index 0000000..2576b7d
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml
@@ -0,0 +1,47 @@
+<!--
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+    <LinearLayout
+        android:id="@+id/on_gear_change_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <Button
+            android:id="@+id/send_on_gear_change_config"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/send_on_gear_change"/>
+        <Button
+            android:id="@+id/remove_on_gear_change_config"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/remove_on_gear_change"/>
+        <Button
+            android:id="@+id/get_on_gear_change_report"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/get_on_gear_change"/>
+    </LinearLayout>
+    <TextView
+        android:id="@+id/output_textview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20dp"/>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
index a29296c..82c2c30 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
@@ -48,5 +48,40 @@
                 android:padding="20dp"
                 android:text="@string/cluster_stop"
                 android:id="@+id/cluster_stop_button"/>
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:padding="20dp">
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:gravity="center"
+                        android:layout_margin="5dp"
+                        android:text="@string/cluster_activity_state"/>
+                    <RadioGroup
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="horizontal">
+                        <RadioButton
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_margin="5dp"
+                            android:id="@+id/cluster_activity_state_default"
+                            android:text="@string/cluster_activity_state_default"/>
+                        <RadioButton
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_margin="5dp"
+                            android:id="@+id/cluster_activity_state_enabled"
+                            android:text="@string/cluster_activity_state_enabled"/>
+                        <RadioButton
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_margin="5dp"
+                            android:id="@+id/cluster_activity_state_disabled"
+                            android:text="@string/cluster_activity_state_disabled"/>
+                    </RadioGroup>
+            </LinearLayout>
     </LinearLayout>
 </LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
index f1d7d81..ed3f100 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
@@ -44,6 +44,58 @@
             android:background="#334666"
             android:orientation="vertical">
 
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:orientation="horizontal">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="10dp"
+                    android:layout_marginEnd="10dp"
+                    android:layout_gravity="center_vertical"
+                    android:text="Number of messages:"/>
+
+                <NumberPicker
+                    android:id="@+id/number_messages"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="#1da9ff"
+                    android:foreground="?android:attr/selectableItemBackground"
+                    android:textSize="30sp"/>
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="10dp"
+                    android:layout_marginEnd="10dp"
+                    android:layout_gravity="center_vertical"
+                    android:text="Number of people:"/>
+
+                <NumberPicker
+                    android:id="@+id/number_people"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="#1da9ff"
+                    android:foreground="?android:attr/selectableItemBackground"
+                    android:textSize="30sp"/>
+            </LinearLayout>
+
+            <Button
+                android:id="@+id/customizable_message_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:layout_marginBottom="50dp"
+                android:layout_marginStart="10dp"
+                android:layout_marginEnd="10dp"
+                android:background="#1da9ff"
+                android:foreground="?android:attr/selectableItemBackground"
+                android:text="Customizable message notification builder"
+                android:textSize="30sp"/>
+
             <Button
                 android:id="@+id/category_call_button"
                 android:layout_width="wrap_content"
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/qc_viewer.xml b/tests/EmbeddedKitchenSinkApp/res/layout/qc_viewer.xml
new file mode 100644
index 0000000..d48284b
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/qc_viewer.xml
@@ -0,0 +1,43 @@
+<!--
+  ~ 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.
+  -->
+ <LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center_horizontal">
+
+     <EditText
+         android:id="@+id/qc_uri_input"
+         android:layout_width="match_parent"
+         android:layout_height="wrap_content"
+         android:inputType="text"
+         android:singleLine="true"/>
+
+     <Button
+         android:id="@+id/submit_uri_btn"
+         android:layout_width="wrap_content"
+         android:layout_height="wrap_content"
+         android:text="Update QCView"/>
+
+     <com.android.car.qc.view.QCView
+         android:id="@+id/qc_view"
+         android:layout_width="match_parent"
+         android:layout_height="wrap_content"
+         android:gravity="center"
+         android:focusable="false"/>
+
+ </LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 0613b45..d43e715 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -167,6 +167,10 @@
     <string name="cluster_start_activity" translatable="false">Start Nav Activity</string>
     <string name="cluster_start_activity_failed" translatable="false">Failed to start activity in cluster</string>
     <string name="cluster_not_started" translatable="false">Missing navigation focus</string>
+    <string name="cluster_activity_state" translatable="false">Cluster activity state</string>
+    <string name="cluster_activity_state_default" translatable="false">Default</string>
+    <string name="cluster_activity_state_enabled" translatable="false">On</string>
+    <string name="cluster_activity_state_disabled" translatable="false">Off</string>
 
     <!--  input test -->
     <string name="volume_up" translatable="false">Volume +</string>
@@ -377,4 +381,9 @@
     <!-- Fullscreen Activity Test -->
     <string name="nav_to_full_screen" translatable="false">Navigate to Full Screen</string>
     <string name="cancel" translatable="false">Cancel</string>
+
+    <!-- CarTelemetryService Test -->
+    <string name="send_on_gear_change" translatable="false">Send MetricsConfig on_gear_change</string>
+    <string name="remove_on_gear_change" translatable="false">Remove MetricsConfig on_gear_change</string>
+    <string name="get_on_gear_change" translatable="false">Get on_gear_change Report</string>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index beeac2a..fd8819a 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -23,6 +23,7 @@
 import android.car.hardware.hvac.CarHvacManager;
 import android.car.hardware.power.CarPowerManager;
 import android.car.hardware.property.CarPropertyManager;
+import android.car.telemetry.CarTelemetryManager;
 import android.car.watchdog.CarWatchdogManager;
 import android.content.Context;
 import android.content.Intent;
@@ -66,12 +67,14 @@
 import com.google.android.car.kitchensink.power.PowerTestFragment;
 import com.google.android.car.kitchensink.projection.ProjectionFragment;
 import com.google.android.car.kitchensink.property.PropertyTestFragment;
+import com.google.android.car.kitchensink.qc.QCViewerFragment;
 import com.google.android.car.kitchensink.rotary.RotaryFragment;
 import com.google.android.car.kitchensink.sensor.SensorsTestFragment;
 import com.google.android.car.kitchensink.storagelifetime.StorageLifetimeFragment;
 import com.google.android.car.kitchensink.storagevolumes.StorageVolumesFragment;
 import com.google.android.car.kitchensink.systembars.SystemBarsFragment;
 import com.google.android.car.kitchensink.systemfeatures.SystemFeaturesFragment;
+import com.google.android.car.kitchensink.telemetry.CarTelemetryTestFragment;
 import com.google.android.car.kitchensink.touch.TouchTestFragment;
 import com.google.android.car.kitchensink.users.ProfileUserFragment;
 import com.google.android.car.kitchensink.users.UserFragment;
@@ -199,12 +202,14 @@
             new FragmentMenuEntry("profile_user", ProfileUserFragment.class),
             new FragmentMenuEntry("projection", ProjectionFragment.class),
             new FragmentMenuEntry("property test", PropertyTestFragment.class),
+            new FragmentMenuEntry("qc viewer", QCViewerFragment.class),
             new FragmentMenuEntry("rotary", RotaryFragment.class),
             new FragmentMenuEntry("sensors", SensorsTestFragment.class),
             new FragmentMenuEntry("storage lifetime", StorageLifetimeFragment.class),
             new FragmentMenuEntry("storage volumes", StorageVolumesFragment.class),
             new FragmentMenuEntry("system bars", SystemBarsFragment.class),
             new FragmentMenuEntry("system features", SystemFeaturesFragment.class),
+            new FragmentMenuEntry("telemetry", CarTelemetryTestFragment.class),
             new FragmentMenuEntry("touch test", TouchTestFragment.class),
             new FragmentMenuEntry("users", UserFragment.class),
             new FragmentMenuEntry("user restrictions", UserRestrictionsFragment.class),
@@ -223,6 +228,7 @@
     private CarSensorManager mSensorManager;
     private CarAppFocusManager mCarAppFocusManager;
     private CarProjectionManager mCarProjectionManager;
+    private CarTelemetryManager mCarTelemetryManager;
     private CarWatchdogManager mCarWatchdogManager;
     private Object mPropertyManagerReady = new Object();
 
@@ -250,6 +256,10 @@
         return mCarProjectionManager;
     }
 
+    public CarTelemetryManager getCarTelemetryManager() {
+        return mCarTelemetryManager;
+    }
+
     public CarWatchdogManager getCarWatchdogManager() {
         return mCarWatchdogManager;
     }
@@ -411,6 +421,8 @@
                     (CarAppFocusManager) car.getCarManager(Car.APP_FOCUS_SERVICE);
             mCarProjectionManager =
                     (CarProjectionManager) car.getCarManager(Car.PROJECTION_SERVICE);
+            mCarTelemetryManager =
+                    (CarTelemetryManager) car.getCarManager(Car.CAR_TELEMETRY_SERVICE);
             mCarWatchdogManager =
                     (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE);
             mPropertyManagerReady.notifyAll();
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
index ab4e2d9..5599465 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
@@ -34,6 +34,7 @@
 import android.car.cluster.navigation.NavigationState.Step;
 import android.car.cluster.navigation.NavigationState.Timestamp;
 import android.car.navigation.CarNavigationStatusManager;
+import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.util.Log;
@@ -41,6 +42,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
+import android.widget.RadioButton;
 import android.widget.Toast;
 
 import androidx.annotation.IdRes;
@@ -124,6 +126,7 @@
         NavigationStateProto[] navigationStateArray = new NavigationStateProto[1];
 
         navigationStateArray[0] = NavigationStateProto.newBuilder()
+                .setServiceStatus(NavigationStateProto.ServiceStatus.NORMAL)
                 .addSteps(Step.newBuilder()
                         .setManeuver(Maneuver.newBuilder()
                                 .setType(Maneuver.Type.DEPART)
@@ -205,6 +208,13 @@
 
         view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster());
         view.findViewById(R.id.cluster_stop_button).setOnClickListener(v -> stopCluster());
+        view.findViewById(R.id.cluster_activity_state_default).setOnClickListener(v ->
+                changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT));
+        view.findViewById(R.id.cluster_activity_state_enabled).setOnClickListener(v ->
+                changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_ENABLED));
+        view.findViewById(R.id.cluster_activity_state_disabled).setOnClickListener(v ->
+                changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_DISABLED));
+        updateInitialClusterActivityState(view);
 
         mTurnByTurnButton = view.findViewById(R.id.cluster_turn_left_button);
         mTurnByTurnButton.setOnClickListener(v -> toggleSendTurn());
@@ -212,6 +222,36 @@
         return view;
     }
 
+    private void updateInitialClusterActivityState(View view) {
+        PackageManager pm = getContext().getPackageManager();
+        ComponentName clusterActivity =
+                new ComponentName(getContext(), FakeClusterNavigationActivity.class);
+        int currentComponentState = pm.getComponentEnabledSetting(clusterActivity);
+        RadioButton button = view.findViewById(
+                convertClusterActivityStateToViewId(currentComponentState));
+        button.setChecked(true);
+    }
+
+    private int convertClusterActivityStateToViewId(int componentState) {
+        switch (componentState) {
+            case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT:
+                return R.id.cluster_activity_state_default;
+            case PackageManager.COMPONENT_ENABLED_STATE_ENABLED:
+                return R.id.cluster_activity_state_enabled;
+            case PackageManager.COMPONENT_ENABLED_STATE_DISABLED:
+                return R.id.cluster_activity_state_disabled;
+        }
+        throw new IllegalStateException("Unknown component state: " + componentState);
+    }
+
+    private void changeClusterActivityState(int newComponentState) {
+        PackageManager pm = getContext().getPackageManager();
+        ComponentName clusterActivity =
+                new ComponentName(getContext(), FakeClusterNavigationActivity.class);
+        pm.setComponentEnabledSetting(clusterActivity, newComponentState,
+                PackageManager.DONT_KILL_APP);
+    }
+
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         initCarApi();
@@ -271,6 +311,7 @@
             mTimer.cancel();
             mTimer = null;
         }
+        sendTurn(NavigationStateProto.newBuilder().build());
         mTurnByTurnButton.setText(R.string.cluster_start_guidance);
     }
 
@@ -278,13 +319,11 @@
      * Sends one update of the navigation state through the {@link CarNavigationStatusManager}
      */
     private void sendTurn(@NonNull NavigationStateProto state) {
-        try {
+        if (hasFocus()) {
             Bundle bundle = new Bundle();
             bundle.putByteArray("navstate2", state.toByteArray());
-            mCarNavigationStatusManager.sendEvent(1, bundle);
+            mCarNavigationStatusManager.sendNavigationStateChange(bundle);
             Log.i(TAG, "Sending nav state: " + state);
-        } catch (CarNotConnectedException e) {
-            Log.e(TAG, "Failed to send turn information.", e);
         }
     }
 
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
index 3d6d76d..9dbef65 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
@@ -14,6 +14,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.NumberPicker;
 
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationCompat.Action;
@@ -26,7 +27,9 @@
 import com.google.android.car.kitchensink.KitchenSinkActivity;
 import com.google.android.car.kitchensink.R;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * Test fragment that can send all sorts of notifications.
@@ -100,6 +103,7 @@
         initCallButton(view);
         initCustomGroupSummaryButton(view);
         initGroupWithoutSummaryButton(view);
+        initCustomizableMessageButton(view);
 
         return view;
     }
@@ -244,6 +248,83 @@
         });
     }
 
+    private void initCustomizableMessageButton(View view) {
+        NumberPicker messagesPicker = view.findViewById(R.id.number_messages);
+        messagesPicker.setMinValue(1);
+        messagesPicker.setMaxValue(25);
+        messagesPicker.setWrapSelectorWheel(true);
+        NumberPicker peoplePicker = view.findViewById(R.id.number_people);
+        peoplePicker.setMinValue(1);
+        peoplePicker.setMaxValue(25);
+        peoplePicker.setWrapSelectorWheel(true);
+
+        view.findViewById(R.id.customizable_message_button).setOnClickListener(v -> {
+            int id = mCurrentNotificationId++;
+
+            int numPeople = peoplePicker.getValue();
+            int numMessages = messagesPicker.getValue();
+
+            PendingIntent replyIntent = createServiceIntent(id, "reply");
+            PendingIntent markAsReadIntent = createServiceIntent(id, "read");
+
+            List<Person> personList = new ArrayList<>();
+
+            for (int i = 1; i <= numPeople; i++) {
+                personList.add(new Person.Builder()
+                        .setName("Person " + i)
+                        .setIcon(IconCompat.createWithResource(v.getContext(),
+                                i % 2 == 1 ? R.drawable.avatar1 : R.drawable.avatar2))
+                        .build());
+            }
+
+            MessagingStyle messagingStyle =
+                    new MessagingStyle(personList.get(0))
+                            .setConversationTitle("Customizable Group chat");
+            if (personList.size() > 1) {
+                messagingStyle.setGroupConversation(true);
+            }
+
+            int messageNumber = 1;
+            for (int i = 0; i < numMessages; i++) {
+                int personNum = i % numPeople;
+                if (personNum == numPeople - 1) {
+                    messageNumber++;
+                }
+                Person person = personList.get(personNum);
+                String messageText = person.getName() + "'s " + messageNumber + " message";
+                messagingStyle.addMessage(
+                        new MessagingStyle.Message(
+                                messageText,
+                                System.currentTimeMillis(),
+                                person));
+            }
+
+            NotificationCompat.Builder notification = new NotificationCompat
+                    .Builder(mContext, IMPORTANCE_HIGH_ID)
+                    .setContentTitle("Customizable Group chat (Title)")
+                    .setContentText("Customizable Group chat (Text)")
+                    .setShowWhen(true)
+                    .setCategory(Notification.CATEGORY_MESSAGE)
+                    .setSmallIcon(R.drawable.car_ic_mode)
+                    .setStyle(messagingStyle)
+                    .setAutoCancel(true)
+                    .setColor(mContext.getColor(android.R.color.holo_green_light))
+                    .addAction(
+                            new Action.Builder(R.drawable.ic_check_box, "read", markAsReadIntent)
+                                    .setSemanticAction(Action.SEMANTIC_ACTION_MARK_AS_READ)
+                                    .setShowsUserInterface(false)
+                                    .build())
+                    .addAction(
+                            new Action.Builder(R.drawable.ic_check_box, "reply", replyIntent)
+                                    .setSemanticAction(Action.SEMANTIC_ACTION_REPLY)
+                                    .setShowsUserInterface(false)
+                                    .addRemoteInput(new RemoteInput.Builder("input").build())
+                                    .build());
+
+            mManager.notify(id, notification.build());
+        });
+    }
+
     private void initMessagingStyleButtonForDiffPerson(View view) {
         view.findViewById(R.id.category_message_diff_person_button).setOnClickListener(v -> {
             int id = mCurrentNotificationId++;
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/qc/QCViewerFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/qc/QCViewerFragment.java
new file mode 100644
index 0000000..20e96aa
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/qc/QCViewerFragment.java
@@ -0,0 +1,120 @@
+/*
+ * 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.google.android.car.kitchensink.qc;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.android.car.qc.controller.RemoteQCController;
+import com.android.car.qc.view.QCView;
+
+import com.google.android.car.kitchensink.R;
+
+public final class QCViewerFragment extends Fragment {
+    private static final String KEY_CURRENT_URI_STRING = "CURRENT_URI_STRING";
+
+    private RemoteQCController mController;
+    private String mUriString;
+    private EditText mInput;
+    private Button mButton;
+    private QCView mQCView;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState != null) {
+            mUriString = savedInstanceState.getString(KEY_CURRENT_URI_STRING);
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View v = inflater.inflate(R.layout.qc_viewer, container, false);
+        mInput = v.findViewById(R.id.qc_uri_input);
+        mButton = v.findViewById(R.id.submit_uri_btn);
+        mQCView = v.findViewById(R.id.qc_view);
+
+        mButton.setOnClickListener(v1 -> {
+            mUriString = mInput.getText().toString();
+            Uri uri = Uri.parse(mUriString);
+            updateQCView(uri);
+        });
+
+        if (mUriString != null) {
+            Uri uri = Uri.parse(mUriString);
+            updateQCView(uri);
+        }
+
+        return v;
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putString(KEY_CURRENT_URI_STRING, mUriString);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        if (mController != null) {
+            mController.listen(true);
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mController != null) {
+            mController.listen(false);
+        }
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        mUriString = null;
+        if (mController != null) {
+            mController.destroy();
+            mController = null;
+        }
+    }
+
+    private void updateQCView(Uri uri) {
+        if (uri == null) {
+            return;
+        }
+        if (mController != null) {
+            // destroy old controller
+            mController.destroy();
+        }
+
+        mController = new RemoteQCController(getContext(), uri);
+        mController.addObserver(mQCView);
+        mController.listen(true);
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java
new file mode 100644
index 0000000..7c4a4b1
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java
@@ -0,0 +1,170 @@
+/*
+ * 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.google.android.car.kitchensink.telemetry;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.car.telemetry.CarTelemetryManager;
+import android.car.telemetry.MetricsConfigKey;
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+
+import com.android.car.telemetry.TelemetryProto;
+
+import com.google.android.car.kitchensink.KitchenSinkActivity;
+import com.google.android.car.kitchensink.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class CarTelemetryTestFragment extends Fragment {
+    private static final String LUA_SCRIPT_ON_GEAR_CHANGE =
+            "function onGearChange(state)\n"
+                    + "    result = {data = \"Hello World!\"}\n"
+                    + "    on_script_finished(result)\n"
+                    + "end\n";
+    private static final TelemetryProto.Publisher VEHICLE_PROPERTY_PUBLISHER =
+            TelemetryProto.Publisher.newBuilder()
+                    .setVehicleProperty(
+                            TelemetryProto.VehiclePropertyPublisher.newBuilder()
+                                    .setVehiclePropertyId(VehicleProperty.GEAR_SELECTION)
+                                    .setReadRate(0f)
+                                    .build()
+                    ).build();
+    private static final TelemetryProto.Subscriber VEHICLE_PROPERTY_SUBSCRIBER =
+            TelemetryProto.Subscriber.newBuilder()
+                    .setHandler("onGearChange")
+                    .setPublisher(VEHICLE_PROPERTY_PUBLISHER)
+                    .setPriority(0)
+                    .build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_ON_GEAR_CHANGE_V1 =
+            TelemetryProto.MetricsConfig.newBuilder()
+                    .setName("my_metrics_config")
+                    .setVersion(1)
+                    .setScript(LUA_SCRIPT_ON_GEAR_CHANGE)
+                    .addSubscribers(VEHICLE_PROPERTY_SUBSCRIBER)
+                    .build();
+    private static final MetricsConfigKey KEY_V1 = new MetricsConfigKey(
+            METRICS_CONFIG_ON_GEAR_CHANGE_V1.getName(),
+            METRICS_CONFIG_ON_GEAR_CHANGE_V1.getVersion());
+
+    private final Executor mExecutor = Executors.newSingleThreadExecutor();
+
+    private CarTelemetryManager mCarTelemetryManager;
+    private CarTelemetryResultsListenerImpl mListener;
+    private KitchenSinkActivity mActivity;
+    private TextView mOutputTextView;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        mActivity = (KitchenSinkActivity) getActivity();
+        mCarTelemetryManager = mActivity.getCarTelemetryManager();
+        mListener = new CarTelemetryResultsListenerImpl();
+        mCarTelemetryManager.setListener(mExecutor, mListener);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.car_telemetry_test, container, false);
+
+        mOutputTextView = view.findViewById(R.id.output_textview);
+        Button sendGearConfigBtn = view.findViewById(R.id.send_on_gear_change_config);
+        Button getGearReportBtn = view.findViewById(R.id.get_on_gear_change_report);
+        Button removeGearConfigBtn = view.findViewById(R.id.remove_on_gear_change_config);
+
+        sendGearConfigBtn.setOnClickListener(this::onSendGearChangeConfigBtnClick);
+        removeGearConfigBtn.setOnClickListener(this::onRemoveGearChangeConfigBtnClick);
+        getGearReportBtn.setOnClickListener(this::onGetGearChangeReportBtnClick);
+
+        return view;
+    }
+
+    private void showOutput(String s) {
+        mActivity.runOnUiThread(() -> mOutputTextView.setText(s));
+    }
+
+    private void onSendGearChangeConfigBtnClick(View view) {
+        showOutput("Sending MetricsConfig that listen for gear change...");
+        mCarTelemetryManager.addMetricsConfig(KEY_V1,
+                METRICS_CONFIG_ON_GEAR_CHANGE_V1.toByteArray());
+    }
+
+    private void onRemoveGearChangeConfigBtnClick(View view) {
+        showOutput("Removing MetricsConfig that listens for gear change...");
+        mCarTelemetryManager.removeMetricsConfig(KEY_V1);
+    }
+
+    private void onGetGearChangeReportBtnClick(View view) {
+        showOutput("Fetching report... If nothing shows up after a few seconds, "
+                + "then no result exists");
+        mCarTelemetryManager.sendFinishedReports(KEY_V1);
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+    }
+
+    /**
+     * Implementation of the {@link CarTelemetryManager.CarTelemetryResultsListener}. They update
+     * the view to show the outputs from the APIs of {@link CarTelemetryManager}.
+     * The callbacks are executed in {@link mExecutor}.
+     */
+    private final class CarTelemetryResultsListenerImpl
+            implements CarTelemetryManager.CarTelemetryResultsListener {
+
+        @Override
+        public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
+            PersistableBundle bundle;
+            try (ByteArrayInputStream bis = new ByteArrayInputStream(result)) {
+                bundle = PersistableBundle.readFromStream(bis);
+            } catch (IOException e) {
+                bundle = null;
+            }
+            showOutput("Result is " + bundle.toString());
+        }
+
+        @Override
+        public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
+        }
+
+        @Override
+        public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key, int statusCode) {
+            showOutput("Add MetricsConfig status: " + statusCode);
+        }
+
+        @Override
+        public void onRemoveMetricsConfigStatus(@NonNull MetricsConfigKey key, boolean success) {
+            showOutput("Remove MetricsConfig status: " + success);
+        }
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
index 4493806..f8d19d6 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
@@ -66,25 +66,26 @@
         }
         if (mVolumeList[position] != null) {
             vh.id.setText(mVolumeList[position].id);
-            vh.maxVolume.setText(String.valueOf(mVolumeList[position].maxGain));
             vh.currentVolume.setText(String.valueOf(mVolumeList[position].currentGain));
             int color = mVolumeList[position].hasAudioFocus ? Color.GREEN : Color.GRAY;
             vh.requestButton.setBackgroundColor(color);
             if (position == 0) {
+                vh.maxVolume.setText("Max");
                 vh.upButton.setVisibility(View.INVISIBLE);
                 vh.downButton.setVisibility(View.INVISIBLE);
                 vh.requestButton.setVisibility(View.INVISIBLE);
                 vh.muteButton.setVisibility(View.INVISIBLE);
             } else {
+                vh.maxVolume.setText(String.valueOf(mVolumeList[position].maxGain));
                 vh.upButton.setVisibility(View.VISIBLE);
                 vh.downButton.setVisibility(View.VISIBLE);
                 vh.requestButton.setVisibility(View.VISIBLE);
                 vh.muteButton.setVisibility(mGroupMuteEnabled ? View.VISIBLE : View.INVISIBLE);
                 vh.upButton.setOnClickListener((view) -> {
-                    mFragment.adjustVolumeByOne(mVolumeList[position].groupId, true);
+                    mFragment.adjustVolumeUp(mVolumeList[position].groupId);
                 });
                 vh.downButton.setOnClickListener((view) -> {
-                    mFragment.adjustVolumeByOne(mVolumeList[position].groupId, false);
+                    mFragment.adjustVolumeDown(mVolumeList[position].groupId);
                 });
                 vh.muteButton.setChecked(mVolumeList[position].isMuted);
                 vh.muteButton.setOnClickListener((view) -> {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java
index 675f495..2419511 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java
@@ -52,7 +52,10 @@
     private static final int MSG_REQUEST_FOCUS = 1;
     private static final int MSG_FOCUS_CHANGED = 2;
     private static final int MSG_STOP_RINGTONE = 3;
+    private static final int MSG_ADJUST_VOLUME = 4;
     private static final long RINGTONE_STOP_TIME_MS = 3_000;
+    private static final int ADJUST_VOLUME_UP = 0;
+    private static final int ADJUST_VOLUME_DOWN = 1;
 
     private final int mZoneId;
     private final Object mLock = new Object();
@@ -68,10 +71,19 @@
     @GuardedBy("mLock")
     private Ringtone mRingtone;
 
-    public void sendVolumeChangedMessage(int groupId, int flags) {
+    void sendVolumeChangedMessage(int groupId, int flags) {
         mHandler.sendMessage(mHandler.obtainMessage(MSG_VOLUME_CHANGED, groupId, flags));
     }
 
+    void adjustVolumeUp(int groupId) {
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_ADJUST_VOLUME, groupId, ADJUST_VOLUME_UP));
+    }
+
+    void adjustVolumeDown(int groupId) {
+        mHandler.sendMessage(mHandler
+                .obtainMessage(MSG_ADJUST_VOLUME, groupId, ADJUST_VOLUME_DOWN));
+    }
+
     private class VolumeHandler extends Handler {
         private AudioFocusListener mFocusListener;
 
@@ -105,10 +117,12 @@
                     mVolumeInfos[mGroupIdIndexMap.get(focusGroupId)].hasAudioFocus = true;
                     mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos);
                     break;
-                default :
-                    Log.wtf(TAG,"VolumeHandler handleMessage called with unknown message"
+                case MSG_ADJUST_VOLUME:
+                    adjustVolumeByOne(msg.arg1, msg.arg2 == ADJUST_VOLUME_UP);
+                    break;
+                default:
+                    Log.wtf(TAG, "VolumeHandler handleMessage called with unknown message"
                             + msg.what);
-
             }
         }
     }
@@ -144,7 +158,6 @@
         CarAudioZoneVolumeInfo titlesInfo = new CarAudioZoneVolumeInfo();
         titlesInfo.id = "Group id";
         titlesInfo.currentGain = "Current";
-        titlesInfo.maxGain = "Max";
         mVolumeInfos[0] = titlesInfo;
 
         int i = 1;
@@ -155,13 +168,14 @@
             volumeInfo.id = String.valueOf(groupId);
             int current = mCarAudioManager.getGroupVolume(mZoneId, groupId);
             int max = mCarAudioManager.getGroupMaxVolume(mZoneId, groupId);
+            int min = mCarAudioManager.getGroupMinVolume(mZoneId, groupId);
             volumeInfo.currentGain = String.valueOf(current);
-            volumeInfo.maxGain = String.valueOf(max);
+            volumeInfo.maxGain = max;
+            volumeInfo.minGain = min;
             volumeInfo.isMuted = mCarAudioManager.isVolumeGroupMuted(mZoneId, groupId);
 
             mVolumeInfos[i] = volumeInfo;
-            if (DEBUG)
-            {
+            if (DEBUG) {
                 Log.d(TAG, groupId + " max: " + volumeInfo.maxGain + " current: "
                         + volumeInfo.currentGain + " is muted " + volumeInfo.isMuted);
             }
@@ -170,18 +184,38 @@
         mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos);
     }
 
-    public void adjustVolumeByOne(int groupId, boolean up) {
+    private void adjustVolumeByOne(int groupId, boolean up) {
         if (mCarAudioManager == null) {
             Log.e(TAG, "CarAudioManager is null");
             return;
         }
         int current = mCarAudioManager.getGroupVolume(mZoneId, groupId);
-        int volume = current + (up ? 1 : -1);
-        mCarAudioManager.setGroupVolume(mZoneId, groupId, volume, AudioManager.FLAG_SHOW_UI);
-        if (DEBUG) {
-            Log.d(TAG, "Set group " + groupId + " volume " + volume + " in audio zone "
-                    + mZoneId);
+        CarAudioZoneVolumeInfo info = getVolumeInfo(groupId);
+        int volume = up ? current + 1 : current - 1;
+        if (volume > info.maxGain) {
+            if (DEBUG) {
+                Log.d(TAG, "Reached " + groupId + " max volume "
+                        + " limit " + volume);
+            }
+            return;
         }
+        if (volume < info.minGain) {
+            if (DEBUG) {
+                Log.d(TAG, "Reached " + groupId + " min volume "
+                        + " limit " + volume);
+            }
+            return;
+        }
+        mCarAudioManager.setGroupVolume(mZoneId, groupId, volume, /* flags= */ 0);
+        if (DEBUG) {
+            Log.d(TAG, "Set group " + groupId + " volume "
+                    + mCarAudioManager.getGroupVolume(mZoneId, groupId)
+                    + " in audio zone " + mZoneId);
+        }
+    }
+
+    private CarAudioZoneVolumeInfo getVolumeInfo(int groupId) {
+        return mVolumeInfos[mGroupIdIndexMap.get(groupId)];
     }
 
     public void toggleMute(int groupId) {
@@ -197,7 +231,7 @@
         }
     }
 
-    public void requestFocus(int groupId) {
+    void requestFocus(int groupId) {
         // Automatic volume change only works for primary audio zone.
         if (mZoneId == CarAudioManager.PRIMARY_AUDIO_ZONE) {
             mHandler.sendMessage(mHandler
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
index 2a2c38e..821d362 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
@@ -62,7 +62,8 @@
     public static final class CarAudioZoneVolumeInfo {
         public int groupId;
         public String id;
-        public String maxGain;
+        public int maxGain;
+        public int minGain;
         public String currentGain;
         public boolean hasAudioFocus;
         public boolean isMuted;
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
index 33bc54a..36a300e 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
@@ -73,6 +73,7 @@
     private static final long TEN_MEGABYTES = 1024 * 1024 * 10;
     private static final int DISK_DELAY_MS = 3000;
     private static final String TAG = "CarWatchdogTestFragment";
+    private static final double WARN_THRESHOLD_PERCENT = 0.8;
     private static final double EXCEED_WARN_THRESHOLD_PERCENT = 0.9;
 
     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
@@ -133,13 +134,23 @@
                                         return;
                                     }
 
+                                    /*
+                                     * CarService notifies applications on exceeding 80% of the
+                                     * threshold. The app maybe notified before completing the
+                                     * following write. Ergo, the minimum expected written bytes
+                                     * should be the warn threshold rather than the actual amount
+                                     * of bytes written by the app.
+                                     */
+                                    long bytesToWarnThreshold = (long) Math.ceil(
+                                            (remainingBytes + TEN_MEGABYTES)
+                                                    * WARN_THRESHOLD_PERCENT);
+
+                                    listener.setExpectedMinWrittenBytes(bytesToWarnThreshold);
+
                                     long bytesToExceedWarnThreshold =
                                             (long) Math.ceil(remainingBytes
                                                     * EXCEED_WARN_THRESHOLD_PERCENT);
 
-                                    listener.setExpectedMinWrittenBytes(
-                                            TEN_MEGABYTES + bytesToExceedWarnThreshold);
-
                                     if (!writeToDisk(bytesToExceedWarnThreshold)) {
                                         mCarWatchdogManager.removeResourceOveruseListener(listener);
                                         return;
diff --git a/tests/NetworkPreferenceApp/Android.bp b/tests/NetworkPreferenceApp/Android.bp
index 22c5f85..e378450 100644
--- a/tests/NetworkPreferenceApp/Android.bp
+++ b/tests/NetworkPreferenceApp/Android.bp
@@ -43,7 +43,6 @@
 
     static_libs: [
         "vehicle-hal-support-lib",
-        "car-experimental-api-static-lib",
         "androidx.legacy_legacy-support-v4",
     ],
 
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/NetworkPreferenceApp/src/com/google/android/car/networking/preferenceupdater/components/CarDriverDistractionManagerAdapter.java b/tests/NetworkPreferenceApp/src/com/google/android/car/networking/preferenceupdater/components/CarDriverDistractionManagerAdapter.java
index eb4d527..b2a096f 100644
--- a/tests/NetworkPreferenceApp/src/com/google/android/car/networking/preferenceupdater/components/CarDriverDistractionManagerAdapter.java
+++ b/tests/NetworkPreferenceApp/src/com/google/android/car/networking/preferenceupdater/components/CarDriverDistractionManagerAdapter.java
@@ -16,9 +16,7 @@
 package com.google.android.car.networking.preferenceupdater.components;
 
 import android.car.Car;
-import android.car.experimental.CarDriverDistractionManager;
-import android.car.experimental.DriverDistractionChangeEvent;
-import android.car.experimental.ExperimentalCar;
+import android.car.drivingstate.CarUxRestrictionsManager;
 import android.content.Context;
 
 /**
@@ -26,21 +24,15 @@
  * information about about driving state to the caller.
  */
 public final class CarDriverDistractionManagerAdapter {
-    private final CarDriverDistractionManager mCarDriverDistractionManager;
+    private final CarUxRestrictionsManager mCarUxRestrictionsManager;
     private final Car mCar;
 
     public CarDriverDistractionManagerAdapter(Context ctx) {
         // Connect to car service
         mCar = Car.createCar(ctx);
-        if (mCar.isFeatureEnabled(
-                ExperimentalCar.DRIVER_DISTRACTION_EXPERIMENTAL_FEATURE_SERVICE)) {
-            mCarDriverDistractionManager = (CarDriverDistractionManager) mCar.getCarManager(
-                    ExperimentalCar.DRIVER_DISTRACTION_EXPERIMENTAL_FEATURE_SERVICE);
-        } else {
-            mCarDriverDistractionManager = null;
-        }
+        mCarUxRestrictionsManager = (CarUxRestrictionsManager) mCar.getCarManager(
+                Car.CAR_UX_RESTRICTION_SERVICE);
     }
-
     /** Method that has to be called during destroy. */
     public void destroy() {
         mCar.disconnect();
@@ -50,24 +42,8 @@
      * Returns true/false boolean based on the whether driver can be distracted right now or not
      */
     public boolean allowedToBeDistracted() {
-        if (mCarDriverDistractionManager == null) {
-            // This means we could not bind to CarDriverDistractionManager. Return true.
-            return true;
-        }
-        DriverDistractionChangeEvent event = mCarDriverDistractionManager.getLastDistractionEvent();
-        /**
-         * event.getAwarenessPercentage returns the current driver awareness value, a float number
-         * between 0.0 -> 1.0, which represents a percentage of the required awareness from driver.
-         *  - 0.0 indicates that the driver has no situational awareness of the surrounding
-         *    environment, which means driver is allawed to be distracted.
-         *  - 1.0 indicates that the driver has the target amount of situational awareness
-         *    necessary for the current driving environment, meaning driver can not be distracted at
-         *    this moment.
-         */
-        if (event.getAwarenessPercentage() > 0) {
-            // To simplify logic, we consider everything above 0% as dangerous and return false.
-            return false;
-        }
-        return true;
+        return mCarUxRestrictionsManager == null
+                || !mCarUxRestrictionsManager.getCurrentCarUxRestrictions()
+                        .isRequiresDistractionOptimization();
     }
 }
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/UserSwitchMonitorApp/Android.bp b/tests/UserSwitchMonitorApp/Android.bp
index 9aa39a9..b5f29eb 100644
--- a/tests/UserSwitchMonitorApp/Android.bp
+++ b/tests/UserSwitchMonitorApp/Android.bp
@@ -27,3 +27,18 @@
 
     sdk_version: "system_current",
 }
+
+// "Cloned" app used to make sure events are received by apps with shared uid
+android_app {
+    name: "UserSwitchMonitorApp2",
+
+    manifest: "AndroidManifest2.xml",
+
+    libs: [
+        "android.car-system-stubs",
+    ],
+
+    srcs: ["src/**/*.java"],
+
+    sdk_version: "system_current",
+}
diff --git a/tests/UserSwitchMonitorApp/AndroidManifest.xml b/tests/UserSwitchMonitorApp/AndroidManifest.xml
index e78d690..fec167a 100644
--- a/tests/UserSwitchMonitorApp/AndroidManifest.xml
+++ b/tests/UserSwitchMonitorApp/AndroidManifest.xml
@@ -16,15 +16,17 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.google.android.car.userswitchmonitor">
+    package="com.google.android.car.userswitchmonitor"
+    android:sharedUserId="com.google.android.car.userswitchmonitor">
 
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
 
-    <application android:label="User Switch Monitor">
-        <service android:name=".UserSwitchMonitorService"/>
-        <receiver android:name=".BootCompletedReceiver" android:exported="true">
+    <application android:icon="@drawable/ic_launcher" android:label="User Switch Monitor">
+        <service android:name="com.google.android.car.userswitchmonitor.UserSwitchMonitorService"/>
+        <receiver android:name="com.google.android.car.userswitchmonitor.BootCompletedReceiver"
+                  android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED"/>
             </intent-filter>
diff --git a/tests/UserSwitchMonitorApp/AndroidManifest2.xml b/tests/UserSwitchMonitorApp/AndroidManifest2.xml
new file mode 100644
index 0000000..bb38752
--- /dev/null
+++ b/tests/UserSwitchMonitorApp/AndroidManifest2.xml
@@ -0,0 +1,36 @@
+<?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
+  -->
+
+<!-- "Cloned" app used to make sure events are received by apps with shared uid -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.car.userswitchmonitor2"
+    android:sharedUserId="com.google.android.car.userswitchmonitor">
+
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+
+    <application android:icon="@drawable/ic_launcher" android:label="User Switch Monitor">
+        <service android:name="com.google.android.car.userswitchmonitor.UserSwitchMonitorService"/>
+        <receiver android:name="com.google.android.car.userswitchmonitor.BootCompletedReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+    </application>
+</manifest>
diff --git a/tests/UserSwitchMonitorApp/src/com/google/android/car/userswitchmonitor/UserSwitchMonitorService.java b/tests/UserSwitchMonitorApp/src/com/google/android/car/userswitchmonitor/UserSwitchMonitorService.java
index f76b0d6..73c5cc4 100644
--- a/tests/UserSwitchMonitorApp/src/com/google/android/car/userswitchmonitor/UserSwitchMonitorService.java
+++ b/tests/UserSwitchMonitorApp/src/com/google/android/car/userswitchmonitor/UserSwitchMonitorService.java
@@ -41,6 +41,10 @@
 
     static final String TAG = "UserSwitchMonitor";
 
+    private static final String CMD_HELP = "help";
+    private static final String CMD_REGISTER = "register";
+    private static final String CMD_UNREGISTER = "unregister";
+
     private final Object mLock = new Object();
 
     private final int mUserId = android.os.Process.myUserHandle().getIdentifier();
@@ -64,11 +68,16 @@
         mContext = getApplicationContext();
         mCar = Car.createCar(mContext);
         mCarUserManager = (CarUserManager) mCar.getCarManager(Car.CAR_USER_SERVICE);
-        mCarUserManager.addListener((r)-> r.run(), mListener);
+        registerListener();
 
         mNotificationManager = mContext.getSystemService(NotificationManager.class);
     }
 
+    private void registerListener() {
+        Log.d(TAG, "registerListener(): " + mListener);
+        mCarUserManager.addListener((r)-> r.run(), mListener);
+    }
+
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         Log.d(TAG, "onStartCommand(" + mUserId + "): " + intent);
@@ -79,11 +88,13 @@
                 NotificationManager.IMPORTANCE_MIN);
         mNotificationManager.createNotificationChannel(channel);
 
+        // Cannot use R.drawable because package name is different on app2
+        int iconResId = mContext.getApplicationInfo().icon;
         startForeground(startId,
                 new Notification.Builder(mContext, channelId)
                         .setContentText(name)
                         .setContentTitle(name)
-                        .setSmallIcon(R.drawable.ic_launcher)
+                        .setSmallIcon(iconResId)
                         .build());
 
         return super.onStartCommand(intent, flags, startId);
@@ -93,19 +104,29 @@
     public void onDestroy() {
         Log.d(TAG, "onDestroy(" + mUserId + ")");
 
-        if (mCarUserManager != null) {
-            mCarUserManager.removeListener(mListener);
-        } else {
-            Log.w(TAG, "Cannot remove listener because manager is null");
-        }
+        unregisterListener();
         if (mCar != null && mCar.isConnected()) {
             mCar.disconnect();
         }
         super.onDestroy();
     }
 
+    private void unregisterListener() {
+        Log.d(TAG, "unregisterListener(): " + mListener);
+        if (mCarUserManager != null) {
+            mCarUserManager.removeListener(mListener);
+        } else {
+            Log.w(TAG, "Cannot remove listener because manager is null");
+        }
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (args != null && args.length > 0) {
+            executeCommand(pw, args);
+            return;
+        }
+
         pw.printf("User id: %d\n", mUserId);
         synchronized (mLock) {
             if (mEvents.isEmpty()) {
@@ -127,4 +148,48 @@
         return null;
     }
 
+    private void executeCommand(PrintWriter pw, String[] args) {
+        String cmd = args[0];
+        switch (cmd) {
+            case CMD_HELP:
+                cmdHelp(pw);
+                break;
+            case CMD_REGISTER:
+                cmdRegister(pw);
+                break;
+            case CMD_UNREGISTER:
+                cmdUnregister(pw);
+                break;
+            default:
+                pw.printf("invalid command: %s\n\n",  cmd);
+                cmdHelp(pw);
+        }
+    }
+
+    private void cmdHelp(PrintWriter pw) {
+        pw.printf("Options:\n");
+        pw.printf("  help: show this help\n");
+        pw.printf("  register: register the service to receive events\n");
+        pw.printf("  unregister: unregister the service from receiving events\n");
+    }
+
+
+    private void cmdRegister(PrintWriter pw) {
+        pw.printf("registering listener %s\n", mListener);
+        runCmd(pw, () -> registerListener());
+    }
+
+    private void cmdUnregister(PrintWriter pw) {
+        pw.printf("unregistering listener %s\n", mListener);
+        runCmd(pw, () -> unregisterListener());
+    }
+
+    private void runCmd(PrintWriter pw, Runnable r) {
+        try {
+            r.run();
+        } catch (Exception e) {
+            Log.e(TAG, "error running command", e);
+            pw.printf("failed: %s\n", e);
+        }
+    }
 }
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
index af81c61..276e829 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
@@ -36,6 +36,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
@@ -246,6 +248,24 @@
         APP_FOCUS_TYPE_NAVIGATION, false)).isTrue();
     }
 
+    @Test
+    public void testGetAppTypeOwner() throws Exception {
+        CarAppFocusManager manager = createManager(getContext(), mEventThread);
+
+        assertThat(manager.getAppTypeOwner(APP_FOCUS_TYPE_NAVIGATION)).isNull();
+
+        FocusOwnershipCallback owner = new FocusOwnershipCallback();
+        assertThat(manager.requestAppFocus(APP_FOCUS_TYPE_NAVIGATION, owner))
+                .isEqualTo(APP_FOCUS_REQUEST_SUCCEEDED);
+
+        assertThat(manager.getAppTypeOwner(APP_FOCUS_TYPE_NAVIGATION))
+                .containsExactly("android.car.apitest", "com.google.android.car.kitchensink");
+
+        manager.abandonAppFocus(owner, APP_FOCUS_TYPE_NAVIGATION);
+
+        assertThat(manager.getAppTypeOwner(APP_FOCUS_TYPE_NAVIGATION)).isNull();
+    }
+
     private CarAppFocusManager createManager() throws InterruptedException {
         return createManager(getContext(), mEventThread);
     }
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/android_car_api_test/src/android/car/apitest/CarUserManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarUserManagerTest.java
index 62f2b0a..2662eaf 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarUserManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarUserManagerTest.java
@@ -18,15 +18,22 @@
 import static android.car.test.util.UserTestingHelper.clearUserLockCredentials;
 import static android.car.test.util.UserTestingHelper.setMaxSupportedUsers;
 import static android.car.test.util.UserTestingHelper.setUserLockCredentials;
+import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STARTING;
 import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
 import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import android.annotation.UserIdInt;
 import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.car.Car;
 import android.car.testapi.BlockingUserLifecycleListener;
+import android.car.user.CarUserManager;
 import android.car.user.CarUserManager.UserLifecycleEvent;
 import android.content.pm.UserInfo;
+import android.os.Process;
+import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
 
@@ -34,12 +41,15 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import java.util.List;
+
 public final class CarUserManagerTest extends CarMultiUserTestBase {
 
     private static final String TAG = CarUserManagerTest.class.getSimpleName();
 
     private static final int PIN = 2345;
 
+    private static final int START_TIMEOUT_MS = 20_000;
     private static final int SWITCH_TIMEOUT_MS = 70_000;
 
     private static final int sMaxNumberUsersBefore = UserManager.getMaxSupportedUsers();
@@ -92,6 +102,75 @@
         assertUserInfo(newGuest, loadedGuest);
     }
 
+    @Test
+    public void testLifecycleMultipleListeners() throws Exception {
+        int newUserId = createUser("Test").id;
+        Car car2 = Car.createCar(getContext().getApplicationContext());
+        CarUserManager mgr2 = (CarUserManager) car2.getCarManager(Car.CAR_USER_SERVICE);
+        CarUserManager mgr1 = mCarUserManager;
+        Log.d(TAG, "myUid=" + Process.myUid() + ",mgr1=" + mgr1 + ", mgr2=" + mgr2);
+        assertWithMessage("mgrs").that(mgr1).isNotSameInstanceAs(mgr2);
+
+        BlockingUserLifecycleListener listener1 = BlockingUserLifecycleListener
+                .forSpecificEvents()
+                .forUser(newUserId)
+                .setTimeout(START_TIMEOUT_MS)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+                .build();
+        BlockingUserLifecycleListener listener2 = BlockingUserLifecycleListener
+                .forSpecificEvents()
+                .forUser(newUserId)
+                .setTimeout(START_TIMEOUT_MS)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+                .build();
+
+        Log.d(TAG, "registering listener1: " + listener1);
+        mgr1.addListener(Runnable::run, listener1);
+        Log.v(TAG, "ok");
+        try {
+            Log.d(TAG, "registering listener2: " + listener2);
+            mgr2.addListener(Runnable::run, listener2);
+            Log.v(TAG, "ok");
+            try {
+                IActivityManager am = ActivityManager.getService();
+                Log.d(TAG, "Starting user " + newUserId);
+                am.startUserInBackground(newUserId);
+                Log.v(TAG, "ok");
+
+                Log.d(TAG, "Waiting for events");
+                List<UserLifecycleEvent> events1 = listener1.waitForEvents();
+                Log.d(TAG, "events1: " + events1);
+                List<UserLifecycleEvent> events2 = listener2.waitForEvents();
+                Log.d(TAG, "events2: " + events2);
+                assertStartUserEvent(events1, newUserId);
+                assertStartUserEvent(events2, newUserId);
+            } finally {
+                Log.d(TAG, "unregistering listener2: " + listener2);
+                mgr2.removeListener(listener2);
+                Log.v(TAG, "ok");
+            }
+        } finally {
+            Log.d(TAG, "unregistering listener1: " + listener1);
+            mgr1.removeListener(listener1);
+            Log.v(TAG, "ok");
+        }
+    }
+
+    private void assertStartUserEvent(List<UserLifecycleEvent> events, @UserIdInt int userId) {
+        assertWithMessage("events").that(events).hasSize(1);
+
+        UserLifecycleEvent event = events.get(0);
+        assertWithMessage("type").that(event.getEventType())
+                .isEqualTo(USER_LIFECYCLE_EVENT_TYPE_STARTING);
+        assertWithMessage("user id on %s", event).that(event.getUserId()).isEqualTo(userId);
+        assertWithMessage("user handle on %s", event).that(event.getUserHandle().getIdentifier())
+                .isEqualTo(userId);
+        assertWithMessage("previous user id on %s", event).that(event.getPreviousUserId())
+                .isEqualTo(UserHandle.USER_NULL);
+        assertWithMessage("previous user handle on %s", event).that(event.getPreviousUserHandle())
+                .isNull();
+    }
+
     /**
      * Tests resume behavior when current user is ephemeral guest, a new guest user should be
      * created and switched to.
diff --git a/tests/android_car_api_test/src/android/car/apitest/media/CarAudioManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/media/CarAudioManagerTest.java
index 9fb2781..cab66f9 100644
--- a/tests/android_car_api_test/src/android/car/apitest/media/CarAudioManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/media/CarAudioManagerTest.java
@@ -16,14 +16,18 @@
 
 package android.car.apitest.media;
 
+import static android.car.Car.AUDIO_SERVICE;
+import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING;
+import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING;
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioAttributes.USAGE_MEDIA;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assume.assumeTrue;
 
-import android.car.Car;
 import android.car.apitest.CarApiTestBase;
 import android.car.media.CarAudioManager;
-import android.media.AudioAttributes;
 import android.media.AudioDeviceInfo;
 import android.os.Process;
 
@@ -38,11 +42,13 @@
 @RunWith(AndroidJUnit4.class)
 public class CarAudioManagerTest extends CarApiTestBase {
 
+    private static final int TEST_FLAGS = 0;
+
     private CarAudioManager mCarAudioManager;
 
     @Before
     public void setUp() throws Exception {
-        mCarAudioManager = (CarAudioManager) getCar().getCarManager(Car.AUDIO_SERVICE);
+        mCarAudioManager = (CarAudioManager) getCar().getCarManager(AUDIO_SERVICE);
         assertThat(mCarAudioManager).isNotNull();
     }
 
@@ -52,21 +58,21 @@
 
         List<Integer> zoneIds = mCarAudioManager.getAudioZoneIds();
         assertThat(zoneIds).isNotEmpty();
-        assertThat(zoneIds).contains(CarAudioManager.PRIMARY_AUDIO_ZONE);
+        assertThat(zoneIds).contains(PRIMARY_AUDIO_ZONE);
     }
 
     @Test
     public void test_isAudioFeatureEnabled() throws Exception {
         // nothing to assert. Just call the API.
-        mCarAudioManager.isAudioFeatureEnabled(CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING);
-        mCarAudioManager.isAudioFeatureEnabled(CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING);
+        mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING);
+        mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING);
     }
 
     @Test
     public void test_getVolumeGroupCount() throws Exception {
         int primaryZoneCount = mCarAudioManager.getVolumeGroupCount();
         assertThat(
-                mCarAudioManager.getVolumeGroupCount(CarAudioManager.PRIMARY_AUDIO_ZONE)).isEqualTo(
+                mCarAudioManager.getVolumeGroupCount(PRIMARY_AUDIO_ZONE)).isEqualTo(
                 primaryZoneCount);
     }
 
@@ -74,14 +80,14 @@
     public void test_getGroupVolume() throws Exception {
         int groudId = 0;
         int volume = mCarAudioManager.getGroupVolume(groudId);
-        assertThat(mCarAudioManager.getGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE,
-                groudId)).isEqualTo(volume);
+        assertThat(mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groudId))
+                .isEqualTo(volume);
         int maxVolume = mCarAudioManager.getGroupMaxVolume(groudId);
-        assertThat(mCarAudioManager.getGroupMaxVolume(CarAudioManager.PRIMARY_AUDIO_ZONE,
-                groudId)).isEqualTo(maxVolume);
+        assertThat(mCarAudioManager.getGroupMaxVolume(PRIMARY_AUDIO_ZONE, groudId))
+                .isEqualTo(maxVolume);
         int minVolume = mCarAudioManager.getGroupMinVolume(groudId);
-        assertThat(mCarAudioManager.getGroupMinVolume(CarAudioManager.PRIMARY_AUDIO_ZONE,
-                groudId)).isEqualTo(minVolume);
+        assertThat(mCarAudioManager.getGroupMinVolume(PRIMARY_AUDIO_ZONE, groudId))
+                .isEqualTo(minVolume);
         assertThat(volume).isAtLeast(minVolume);
         assertThat(volume).isAtMost(maxVolume);
     }
@@ -90,8 +96,8 @@
     public void test_setGroupVolume() throws Exception {
         int groudId = 0;
         int volume = mCarAudioManager.getGroupVolume(groudId);
-        mCarAudioManager.setGroupVolume(groudId, volume, 0);
-        mCarAudioManager.setGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groudId, volume, 0);
+        mCarAudioManager.setGroupVolume(groudId, volume, TEST_FLAGS);
+        mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groudId, volume, TEST_FLAGS);
         assertThat(mCarAudioManager.getGroupVolume(groudId)).isEqualTo(volume);
     }
 
@@ -99,8 +105,7 @@
     public void test_getInputDevicesForZoneId() throws Exception {
         assumeDynamicRoutingIsEnabled();
 
-        List<AudioDeviceInfo> info = mCarAudioManager.getInputDevicesForZoneId(
-                CarAudioManager.PRIMARY_AUDIO_ZONE);
+        List<AudioDeviceInfo> info = mCarAudioManager.getInputDevicesForZoneId(PRIMARY_AUDIO_ZONE);
         assertThat(info).isNotEmpty();
     }
 
@@ -109,15 +114,15 @@
         assumeDynamicRoutingIsEnabled();
 
         AudioDeviceInfo device = mCarAudioManager.getOutputDeviceForUsage(
-                CarAudioManager.PRIMARY_AUDIO_ZONE, AudioAttributes.USAGE_MEDIA);
+                PRIMARY_AUDIO_ZONE, USAGE_MEDIA);
         assertThat(device).isNotNull();
     }
 
     @Test
     public void test_getVolumeGroupIdForUsage() throws Exception {
-        int groupId = mCarAudioManager.getVolumeGroupIdForUsage(AudioAttributes.USAGE_MEDIA);
-        assertThat(mCarAudioManager.getVolumeGroupIdForUsage(CarAudioManager.PRIMARY_AUDIO_ZONE,
-                AudioAttributes.USAGE_MEDIA)).isEqualTo(groupId);
+        int groupId = mCarAudioManager.getVolumeGroupIdForUsage(USAGE_MEDIA);
+        assertThat(mCarAudioManager.getVolumeGroupIdForUsage(PRIMARY_AUDIO_ZONE, USAGE_MEDIA))
+                .isEqualTo(groupId);
         int primaryZoneCount = mCarAudioManager.getVolumeGroupCount();
         assertThat(groupId).isLessThan(primaryZoneCount);
     }
@@ -126,8 +131,99 @@
     public void test_getZoneIdForUid() throws Exception {
         assumeDynamicRoutingIsEnabled();
 
-        assertThat(mCarAudioManager.getZoneIdForUid(Process.myUid())).isEqualTo(
-                CarAudioManager.PRIMARY_AUDIO_ZONE);
+        assertThat(mCarAudioManager.getZoneIdForUid(Process.myUid())).isEqualTo(PRIMARY_AUDIO_ZONE);
+    }
+
+    @Test
+    public void setVolumeGroupMute_toMute_mutesVolumeGroup() throws Exception {
+        assumeVolumeGroupMutingIsEnabled();
+        int groupId = 0;
+        boolean  muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+
+        try {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, true, TEST_FLAGS);
+            assertThat(mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId))
+                    .isEqualTo(true);
+        } finally {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+        }
+    }
+
+    @Test
+    public void setVolumeGroupMute_toUnMute_unMutesVolumeGroup() throws Exception {
+        assumeVolumeGroupMutingIsEnabled();
+        int groupId = 0;
+        boolean  muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+
+        try {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, false, TEST_FLAGS);
+            assertThat(mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId))
+                    .isEqualTo(false);
+        } finally {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+        }
+    }
+
+    @Test
+    public void setGroupVolume_whileMuted_unMutesVolumeGroup() throws Exception {
+        assumeVolumeGroupMutingIsEnabled();
+        int groupId = 0;
+        boolean  muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+        int volume = mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groupId);
+        int minVolume = mCarAudioManager.getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
+
+        try {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, true, TEST_FLAGS);
+
+            mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, minVolume, TEST_FLAGS);
+            assertThat(mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId))
+                    .isEqualTo(false);
+        } finally {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+            mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, volume, TEST_FLAGS);
+        }
+    }
+
+    @Test
+    public void getGroupVolume_whileMuted_returnsMinVolume() throws Exception {
+        assumeVolumeGroupMutingIsEnabled();
+        int groupId = 0;
+        boolean  muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+        int minVolume = mCarAudioManager.getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
+
+        try {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, true, TEST_FLAGS);
+
+            assertThat(mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groupId))
+                    .isEqualTo(minVolume);
+        } finally {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+        }
+    }
+
+    @Test
+    public void getGroupVolume_whileUnMuted_returnLastSetValue() throws Exception {
+        assumeVolumeGroupMutingIsEnabled();
+        int groupId = 0;
+        boolean  muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+        int volume = mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groupId);
+        int minVolume = mCarAudioManager.getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
+        int maxVolume = mCarAudioManager.getGroupMaxVolume(PRIMARY_AUDIO_ZONE, groupId);
+        int testVolume = (minVolume + maxVolume) / 2;
+
+        try {
+            mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, testVolume, TEST_FLAGS);
+
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, true, TEST_FLAGS);
+
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, false, TEST_FLAGS);
+
+            assertThat(mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groupId))
+                    .isEqualTo(testVolume);
+        } finally {
+            mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+            mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, volume, TEST_FLAGS);
+        }
     }
 
     @Test
@@ -136,12 +232,14 @@
 
         // TODO(b/191660867): Better to change this to play something and asert true.
         assertThat(
-                mCarAudioManager.isPlaybackOnVolumeGroupActive(CarAudioManager.PRIMARY_AUDIO_ZONE,
+                mCarAudioManager.isPlaybackOnVolumeGroupActive(PRIMARY_AUDIO_ZONE,
                         0)).isFalse();
     }
 
     private void assumeDynamicRoutingIsEnabled() {
-        assumeTrue(mCarAudioManager.isAudioFeatureEnabled(
-                CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING));
+        assumeTrue(mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING));
+    }
+    private void assumeVolumeGroupMutingIsEnabled() {
+        assumeTrue(mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING));
     }
 }
diff --git a/tests/carservice_test/AndroidManifest.xml b/tests/carservice_test/AndroidManifest.xml
index 7bf749e..47dbb26 100644
--- a/tests/carservice_test/AndroidManifest.xml
+++ b/tests/carservice_test/AndroidManifest.xml
@@ -59,6 +59,11 @@
             <meta-data android:name="distractionOptimized"
                  android:value="true"/>
         </activity>
+        <activity android:name="androidx.car.app.activity.CarAppActivity"
+            android:label="CarAppActivity">
+            <meta-data android:name="distractionOptimized"
+                android:value="true"/>
+        </activity>
 
         <receiver android:name="com.android.car.CarStorageMonitoringBroadcastReceiver"
              android:exported="true"
diff --git a/tests/carservice_test/src/androidx/car/app/activity/CarAppActivity.java b/tests/carservice_test/src/androidx/car/app/activity/CarAppActivity.java
new file mode 100644
index 0000000..b42b7cc
--- /dev/null
+++ b/tests/carservice_test/src/androidx/car/app/activity/CarAppActivity.java
@@ -0,0 +1,90 @@
+/*
+ * 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 androidx.car.app.activity;
+
+
+import static com.android.car.pm.ActivityBlockingActivityTest.DoActivity.DIALOG_TITLE;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+/**
+ * An activity to represent a template activity in tests.
+ */
+public class CarAppActivity extends Activity {
+    public static final String ACTION_SHOW_DIALOG = "SHOW_DIALOG";
+    public static final String ACTION_START_SECOND_INSTANCE = "START_SECOND_INSTANCE";
+    public static final String SECOND_INSTANCE_TITLE = "Second Instance";
+    private static final String BUNDLE_KEY_IS_SECOND_INSTANCE = "is_second_instance";
+
+    private final ShowDialogReceiver mShowDialogReceiver = new ShowDialogReceiver();
+    private final StartSecondInstanceReceiver
+            mStartSecondInstanceReceiver = new StartSecondInstanceReceiver();
+
+    private class ShowDialogReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            showDialog();
+        }
+    }
+
+    private class StartSecondInstanceReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            startSecondInstance();
+        }
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (getIntent().getBooleanExtra(BUNDLE_KEY_IS_SECOND_INSTANCE, false)) {
+            getActionBar().setTitle(SECOND_INSTANCE_TITLE);
+        }
+        this.registerReceiver(mShowDialogReceiver, new IntentFilter(ACTION_SHOW_DIALOG));
+        this.registerReceiver(mStartSecondInstanceReceiver,
+                new IntentFilter(ACTION_START_SECOND_INSTANCE));
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        this.unregisterReceiver(mShowDialogReceiver);
+        this.unregisterReceiver(mStartSecondInstanceReceiver);
+    }
+
+    private void startSecondInstance() {
+        Intent intent = new Intent(CarAppActivity.this, CarAppActivity.class);
+        intent.putExtra(BUNDLE_KEY_IS_SECOND_INSTANCE, true);
+        startActivity(intent);
+    }
+
+    private void showDialog() {
+        AlertDialog dialog = new AlertDialog.Builder(this)
+                .setTitle(DIALOG_TITLE)
+                .setMessage("Message")
+                .create();
+        dialog.show();
+    }
+}
diff --git a/tests/carservice_test/src/com/android/car/AppFocusTest.java b/tests/carservice_test/src/com/android/car/AppFocusTest.java
index 390acf2..1afb52f 100644
--- a/tests/carservice_test/src/com/android/car/AppFocusTest.java
+++ b/tests/carservice_test/src/com/android/car/AppFocusTest.java
@@ -16,6 +16,9 @@
 package com.android.car;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import android.car.Car;
 import android.car.CarAppFocusManager;
@@ -46,10 +49,13 @@
         manager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, ownershipListener);
         listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, true);
+        assertThat(manager.getAppTypeOwner(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION))
+                .containsExactly("com.android.car.test", "com.google.android.car.kitchensink");
         listener.resetWait();
         manager.abandonAppFocus(ownershipListener, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
         listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
                 CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, false);
+        assertNull(manager.getAppTypeOwner(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION));
         manager.removeFocusListener(listener);
     }
 
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/audio/CarAudioZonesHelperLegacyTest.java b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperLegacyTest.java
index e006387..49ca55a 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperLegacyTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperLegacyTest.java
@@ -15,6 +15,10 @@
  */
 package com.android.car.audio;
 
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_FM_TUNER;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -23,6 +27,8 @@
 
 import android.annotation.XmlRes;
 import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
 import android.util.SparseArray;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -65,9 +71,10 @@
 
         RuntimeException exception = expectThrows(RuntimeException.class,
                 () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
-                        carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings));
+                        carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings,
+                        getInputDevices()));
 
-        assertThat(exception.getMessage()).contains("Two addresses map to same bus number:");
+        assertThat(exception).hasMessageThat().contains("Two addresses map to same bus number:");
     }
 
     @Test
@@ -78,9 +85,79 @@
 
         RuntimeException exception = expectThrows(RuntimeException.class,
                 () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
-                carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings));
+                        carAudioDeviceInfos, mMockAudioControlWrapper,
+                        mMockCarAudioSettings, getInputDevices()));
 
-        assertThat(exception.getMessage()).contains("Invalid bus -1 was associated with context");
+        assertThat(exception).hasMessageThat()
+                .contains("Invalid bus -1 was associated with context");
+    }
+
+    @Test
+    public void constructor_throwsIfNullInputDevices() throws Exception {
+        List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+        when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+        NullPointerException exception = expectThrows(NullPointerException.class,
+                () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+                        carAudioDeviceInfos, mMockAudioControlWrapper,
+                        mMockCarAudioSettings, null));
+
+        assertThat(exception).hasMessageThat().contains("Input Devices");
+    }
+
+    @Test
+    public void constructor_throwsIfNullContext() throws Exception {
+        List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+        when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+        NullPointerException exception = expectThrows(NullPointerException.class,
+                () -> new CarAudioZonesHelperLegacy(null, mCarVolumeGroups,
+                        carAudioDeviceInfos, mMockAudioControlWrapper,
+                        mMockCarAudioSettings, getInputDevices()));
+
+        assertThat(exception).hasMessageThat().contains("Context");
+    }
+
+    @Test
+    public void constructor_throwsIfNullCarAudioDeviceInfo() throws Exception {
+        when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+        NullPointerException exception = expectThrows(NullPointerException.class,
+                () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+                        null, mMockAudioControlWrapper,
+                        mMockCarAudioSettings, getInputDevices()));
+
+        assertThat(exception).hasMessageThat().contains("Car Audio Device Info");
+    }
+
+    @Test
+    public void constructor_throwsIfNullCarAudioControl() throws Exception {
+        List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+        when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+        NullPointerException exception = expectThrows(NullPointerException.class,
+                () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+                        carAudioDeviceInfos, null,
+                        mMockCarAudioSettings, getInputDevices()));
+
+        assertThat(exception).hasMessageThat().contains("Car Audio Control");
+    }
+
+    @Test
+    public void constructor_throwsIfNullCarAudioSettings() throws Exception {
+        List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+        when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+        NullPointerException exception = expectThrows(NullPointerException.class,
+                () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+                        carAudioDeviceInfos, mMockAudioControlWrapper,
+                        null, getInputDevices()));
+
+        assertThat(exception).hasMessageThat().contains("Car Audio Settings");
     }
 
     @Test
@@ -89,7 +166,8 @@
         when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(1);
 
         CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
-                carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings);
+                carAudioDeviceInfos, mMockAudioControlWrapper,
+                mMockCarAudioSettings, getInputDevices());
 
         SparseArray<CarAudioZone> zones = helper.loadAudioZones();
 
@@ -103,7 +181,8 @@
         when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(1);
 
         CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
-                carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings);
+                carAudioDeviceInfos, mMockAudioControlWrapper,
+                mMockCarAudioSettings, getInputDevices());
 
         SparseArray<CarAudioZone> zones = helper.loadAudioZones();
         CarVolumeGroup[] volumeGroups = zones.get(0).getVolumeGroups();
@@ -111,6 +190,38 @@
     }
 
     @Test
+    public void loadAudioZones_primaryZoneHasInputDevice() throws Exception {
+        List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+        when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(1);
+
+        CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+                carAudioDeviceInfos, mMockAudioControlWrapper,
+                mMockCarAudioSettings, getInputDevices());
+
+        SparseArray<CarAudioZone> zones = helper.loadAudioZones();
+        CarAudioZone primaryZone = zones.get(PRIMARY_AUDIO_ZONE);
+        assertThat(primaryZone.getInputAudioDevices()).hasSize(1);
+    }
+
+    @Test
+    public void loadAudioZones_primaryZoneHasMicrophoneInputDevice() throws Exception {
+        List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+        when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(1);
+
+        CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+                carAudioDeviceInfos, mMockAudioControlWrapper,
+                mMockCarAudioSettings, getInputDevices());
+
+        SparseArray<CarAudioZone> zones = helper.loadAudioZones();
+        CarAudioZone primaryZone = zones.get(PRIMARY_AUDIO_ZONE);
+        List<AudioDeviceAttributes> audioDeviceInfos =
+                primaryZone.getInputAudioDevices();
+        assertThat(audioDeviceInfos.get(0).getType()).isEqualTo(TYPE_BUILTIN_MIC);
+    }
+
+    @Test
     public void loadAudioZones_associatesLegacyContextsWithCorrectBuses() throws Exception {
         List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
 
@@ -118,7 +229,8 @@
         when(mMockAudioControlWrapper.getBusForContext(CarAudioContext.MUSIC)).thenReturn(1);
 
         CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
-                carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings);
+                carAudioDeviceInfos, mMockAudioControlWrapper,
+                mMockCarAudioSettings, getInputDevices());
 
         SparseArray<CarAudioZone> zones = helper.loadAudioZones();
 
@@ -145,7 +257,8 @@
                 .thenReturn(1);
 
         CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
-                carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings);
+                carAudioDeviceInfos, mMockAudioControlWrapper,
+                mMockCarAudioSettings, getInputDevices());
 
         SparseArray<CarAudioZone> zones = helper.loadAudioZones();
 
@@ -166,6 +279,18 @@
         return Lists.newArrayList(deviceInfo1, deviceInfo2);
     }
 
+    private AudioDeviceInfo[] getInputDevices() {
+        AudioDeviceInfo deviceInfo1 = Mockito.mock(AudioDeviceInfo.class);
+        when(deviceInfo1.getType()).thenReturn(TYPE_BUILTIN_MIC);
+        when(deviceInfo1.getAddress()).thenReturn("mic");
+        when(deviceInfo1.isSink()).thenReturn(false);
+        AudioDeviceInfo deviceInfo2 = Mockito.mock(AudioDeviceInfo.class);
+        when(deviceInfo2.getAddress()).thenReturn("tuner");
+        when(deviceInfo2.getType()).thenReturn(TYPE_FM_TUNER);
+        when(deviceInfo2.isSink()).thenReturn(false);
+        return new AudioDeviceInfo[]{deviceInfo1, deviceInfo2};
+    }
+
     private List<CarAudioDeviceInfo> getValidCarAudioDeviceInfos() {
         CarAudioDeviceInfo deviceInfo1 = Mockito.mock(CarAudioDeviceInfo.class);
         when(deviceInfo1.getAddress()).thenReturn("bus001_media");
diff --git a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperTest.java b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperTest.java
index 74b00c7..14d3ba8 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperTest.java
@@ -15,6 +15,11 @@
  */
 package com.android.car.audio;
 
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_BUS;
+import static android.media.AudioDeviceInfo.TYPE_FM_TUNER;
+
 import static com.android.car.audio.CarAudioService.DEFAULT_AUDIO_CONTEXT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -23,7 +28,6 @@
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.expectThrows;
 
-import android.car.media.CarAudioManager;
 import android.content.Context;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
@@ -104,14 +108,11 @@
 
     private AudioDeviceInfo[] generateInputDeviceInfos() {
         return new AudioDeviceInfo[]{
-                generateInputAudioDeviceInfo(PRIMARY_ZONE_MICROPHONE_ADDRESS,
-                        AudioDeviceInfo.TYPE_BUILTIN_MIC),
-                generateInputAudioDeviceInfo(PRIMARY_ZONE_FM_TUNER_ADDRESS,
-                        AudioDeviceInfo.TYPE_FM_TUNER),
-                generateInputAudioDeviceInfo(SECONDARY_ZONE_BACK_MICROPHONE_ADDRESS,
-                        AudioDeviceInfo.TYPE_BUS),
+                generateInputAudioDeviceInfo(PRIMARY_ZONE_MICROPHONE_ADDRESS, TYPE_BUILTIN_MIC),
+                generateInputAudioDeviceInfo(PRIMARY_ZONE_FM_TUNER_ADDRESS, TYPE_FM_TUNER),
+                generateInputAudioDeviceInfo(SECONDARY_ZONE_BACK_MICROPHONE_ADDRESS, TYPE_BUS),
                 generateInputAudioDeviceInfo(SECONDARY_ZONE_BUS_1000_INPUT_ADDRESS,
-                        AudioDeviceInfo.TYPE_BUILTIN_MIC)
+                        TYPE_BUILTIN_MIC)
         };
     }
 
@@ -169,7 +170,7 @@
 
         List<Integer> zoneIds = getListOfZoneIds(zones);
         assertThat(zones.size()).isEqualTo(2);
-        assertThat(zones.contains(CarAudioManager.PRIMARY_AUDIO_ZONE)).isTrue();
+        assertThat(zones.contains(PRIMARY_AUDIO_ZONE)).isTrue();
         assertThat(zones.contains(SECONDARY_ZONE_ID)).isTrue();
     }
 
@@ -184,7 +185,7 @@
 
         SparseIntArray audioZoneIdToOccupantZoneIdMapping =
                 cazh.getCarAudioZoneIdToOccupantZoneIdMapping();
-        assertThat(audioZoneIdToOccupantZoneIdMapping.get(CarAudioManager.PRIMARY_AUDIO_ZONE))
+        assertThat(audioZoneIdToOccupantZoneIdMapping.get(PRIMARY_AUDIO_ZONE))
                 .isEqualTo(PRIMARY_OCCUPANT_ID);
         assertThat(audioZoneIdToOccupantZoneIdMapping.get(SECONDARY_ZONE_ID, -1))
                 .isEqualTo(-1);
@@ -305,7 +306,7 @@
             SparseArray<CarAudioZone> zones = cazh.loadAudioZones();
 
             assertThat(zones.size()).isEqualTo(2);
-            assertThat(zones.contains(CarAudioManager.PRIMARY_AUDIO_ZONE)).isTrue();
+            assertThat(zones.contains(PRIMARY_AUDIO_ZONE)).isTrue();
             assertThat(zones.contains(SECONDARY_ZONE_ID)).isTrue();
         }
     }
@@ -339,6 +340,30 @@
     }
 
     @Test
+    public void loadAudioZones_primaryZoneHasInputDevices() throws Exception {
+        CarAudioZonesHelper cazh = new CarAudioZonesHelper(mCarAudioSettings, mInputStream,
+                mCarAudioOutputDeviceInfos, mInputAudioDeviceInfos, false);
+
+        SparseArray<CarAudioZone> zones = cazh.loadAudioZones();
+
+        CarAudioZone primaryZone = zones.get(PRIMARY_AUDIO_ZONE);
+        assertThat(primaryZone.getInputAudioDevices()).hasSize(2);
+    }
+
+    @Test
+    public void loadAudioZones_primaryZoneHasMicrophoneDevice() throws Exception {
+        CarAudioZonesHelper cazh = new CarAudioZonesHelper(mCarAudioSettings, mInputStream,
+                mCarAudioOutputDeviceInfos, mInputAudioDeviceInfos, false);
+
+        SparseArray<CarAudioZone> zones = cazh.loadAudioZones();
+
+        CarAudioZone primaryZone = zones.get(PRIMARY_AUDIO_ZONE);
+        for (AudioDeviceAttributes info : primaryZone.getInputAudioDevices()) {
+            assertThat(info.getType()).isEqualTo(TYPE_BUILTIN_MIC);
+        }
+    }
+
+    @Test
     public void loadAudioZones_parsesInputDevices() throws Exception {
         try (InputStream inputDevicesStream = mContext.getResources().openRawResource(
                 R.raw.car_audio_configuration_with_input_devices)) {
diff --git a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesValidatorTest.java b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesValidatorTest.java
index 9c32893..552eedb 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesValidatorTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesValidatorTest.java
@@ -15,9 +15,18 @@
  */
 package com.android.car.audio;
 
-import static org.mockito.Mockito.when;
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_BUS;
+import static android.media.AudioDeviceInfo.TYPE_FM_TUNER;
 
-import android.car.media.CarAudioManager;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+
+import android.media.AudioDeviceAttributes;
 import android.util.SparseArray;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -28,6 +37,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
@@ -37,10 +47,49 @@
 
     @Test
     public void validate_thereIsAtLeastOneZone() {
-        thrown.expect(RuntimeException.class);
-        thrown.expectMessage("At least one zone should be defined");
+        RuntimeException exception = expectThrows(RuntimeException.class,
+                () -> CarAudioZonesValidator.validate(new SparseArray<CarAudioZone>()));
 
-        CarAudioZonesValidator.validate(new SparseArray<CarAudioZone>());
+        assertThat(exception).hasMessageThat().contains("At least one zone should be defined");
+
+    }
+
+    @Test
+    public void validate_failsOnEmptyInputDevices() {
+        CarAudioZone zone = new MockBuilder().withInputDevices(new ArrayList<>()).build();
+        SparseArray<CarAudioZone> zones = new SparseArray<>();
+        zones.put(zone.getId(), zone);
+
+        IllegalArgumentException exception = expectThrows(IllegalArgumentException.class,
+                () -> CarAudioZonesValidator.validate(zones));
+
+        assertThat(exception).hasMessageThat().contains("Primary Zone Input Devices");
+    }
+
+    @Test
+    public void validate_failsOnNullInputDevices() {
+        CarAudioZone zone = new MockBuilder().withInputDevices(null).build();
+        SparseArray<CarAudioZone> zones = new SparseArray<>();
+        zones.put(zone.getId(), zone);
+
+        NullPointerException exception = expectThrows(NullPointerException.class,
+                () -> CarAudioZonesValidator.validate(zones));
+
+        assertThat(exception).hasMessageThat().contains("Primary Zone Input Devices");
+    }
+
+    @Test
+    public void validate_failsOnMissingMicrophoneInputDevices() {
+        CarAudioZone zone = new MockBuilder().withInputDevices(
+                List.of(generateInputAudioDeviceAttributeInfo("tuner", TYPE_FM_TUNER)))
+                .build();
+        SparseArray<CarAudioZone> zones = new SparseArray<>();
+        zones.put(zone.getId(), zone);
+
+        RuntimeException exception = expectThrows(RuntimeException.class,
+                () -> CarAudioZonesValidator.validate(zones));
+
+        assertThat(exception).hasMessageThat().contains("Primary Zone must have");
     }
 
     @Test
@@ -52,10 +101,11 @@
                 .build();
         zones.put(zoneOne.getId(), zoneOne);
 
-        thrown.expect(RuntimeException.class);
-        thrown.expectMessage("Invalid volume groups configuration for zone " + 1);
+        RuntimeException exception = expectThrows(RuntimeException.class,
+                () -> CarAudioZonesValidator.validate(zones));
 
-        CarAudioZonesValidator.validate(zones);
+        assertThat(exception).hasMessageThat()
+                .contains("Invalid volume groups configuration for zone " + 1);
     }
 
     @Test
@@ -77,12 +127,11 @@
         zones.put(primaryZone.getId(), primaryZone);
         zones.put(secondaryZone.getId(), secondaryZone);
 
+        RuntimeException exception = expectThrows(RuntimeException.class,
+                () -> CarAudioZonesValidator.validate(zones));
 
-        thrown.expect(RuntimeException.class);
-        thrown.expectMessage(
+        assertThat(exception).hasMessageThat().contains(
                 "Device with address three appears in multiple volume groups or audio zones");
-
-        CarAudioZonesValidator.validate(zones);
     }
 
     @Test
@@ -93,7 +142,7 @@
     }
 
     private SparseArray<CarAudioZone> generateAudioZonesWithPrimary() {
-        CarAudioZone zone = new MockBuilder().build();
+        CarAudioZone zone = new MockBuilder().withInputDevices(getValidInputDevices()).build();
         SparseArray<CarAudioZone> zones = new SparseArray<>();
         zones.put(zone.getId(), zone);
         return zones;
@@ -105,21 +154,23 @@
         return mockVolumeGroup;
     }
 
-    private CarAudioZone getMockPrimaryZone() {
-        CarAudioZone zoneMock = Mockito.mock(CarAudioZone.class);
-        when(zoneMock.getId()).thenReturn(CarAudioManager.PRIMARY_AUDIO_ZONE);
-        return zoneMock;
+    private List<AudioDeviceAttributes> getValidInputDevices() {
+        return List.of(generateInputAudioDeviceAttributeInfo("mic", TYPE_BUILTIN_MIC),
+                generateInputAudioDeviceAttributeInfo("tuner", TYPE_FM_TUNER),
+                generateInputAudioDeviceAttributeInfo("bus", TYPE_BUS));
     }
     private static class MockBuilder {
         private boolean mHasValidVolumeGroups = true;
-        private int mZoneId = 0;
+        private int mZoneId = PRIMARY_AUDIO_ZONE;
         private CarVolumeGroup[] mVolumeGroups = new CarVolumeGroup[0];
+        private List<AudioDeviceAttributes> mInputDevices = new ArrayList<>();
 
         CarAudioZone build() {
             CarAudioZone zoneMock = Mockito.mock(CarAudioZone.class);
             when(zoneMock.getId()).thenReturn(mZoneId);
             when(zoneMock.validateVolumeGroups()).thenReturn(mHasValidVolumeGroups);
             when(zoneMock.getVolumeGroups()).thenReturn(mVolumeGroups);
+            when(zoneMock.getInputAudioDevices()).thenReturn(mInputDevices);
             return zoneMock;
         }
 
@@ -137,5 +188,17 @@
             mVolumeGroups = volumeGroups;
             return this;
         }
+
+        MockBuilder withInputDevices(List<AudioDeviceAttributes> inputDevices) {
+            mInputDevices = inputDevices;
+            return this;
+        }
+    }
+
+    private AudioDeviceAttributes generateInputAudioDeviceAttributeInfo(String address, int type) {
+        AudioDeviceAttributes inputMock = mock(AudioDeviceAttributes.class);
+        when(inputMock.getAddress()).thenReturn(address);
+        when(inputMock.getType()).thenReturn(type);
+        return inputMock;
     }
 }
\ No newline at end of file
diff --git a/tests/carservice_test/src/com/android/car/garagemode/ControllerTest.java b/tests/carservice_test/src/com/android/car/garagemode/ControllerTest.java
index ca3598a..64c68fc 100644
--- a/tests/carservice_test/src/com/android/car/garagemode/ControllerTest.java
+++ b/tests/carservice_test/src/com/android/car/garagemode/ControllerTest.java
@@ -24,16 +24,21 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.car.Car;
 import android.car.hardware.power.CarPowerManager;
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
@@ -42,6 +47,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.car.CarLocalServices;
+import com.android.car.R;
+import com.android.car.power.CarPowerManagementService;
 import com.android.car.systeminterface.SystemInterface;
 import com.android.car.user.CarUserService;
 
@@ -74,8 +81,10 @@
     @Mock private CarPowerManager mCarPowerManagerMock;
     @Mock private CarUserService mCarUserServiceMock;
     @Mock private SystemInterface mSystemInterfaceMock;
+    @Mock private CarPowerManagementService mCarPowerManagementServiceMock;
     private CarUserService mCarUserServiceOriginal;
     private SystemInterface mSystemInterfaceOriginal;
+    private CarPowerManagementService mCarPowerManagementServiceOriginal;
     @Captor private ArgumentCaptor<Intent> mIntentCaptor;
     @Captor private ArgumentCaptor<Integer> mIntegerCaptor;
 
@@ -106,10 +115,15 @@
         mController.setCarPowerManager(mCarPowerManagerMock);
         mFuture = new CompletableFuture<>();
         mCarUserServiceOriginal = CarLocalServices.getService(CarUserService.class);
+        mCarPowerManagementServiceOriginal = CarLocalServices.getService(
+                CarPowerManagementService.class);
         CarLocalServices.removeServiceForTest(CarUserService.class);
         CarLocalServices.addService(CarUserService.class, mCarUserServiceMock);
         CarLocalServices.removeServiceForTest(SystemInterface.class);
         CarLocalServices.addService(SystemInterface.class, mSystemInterfaceMock);
+        CarLocalServices.removeServiceForTest(CarPowerManagementService.class);
+        CarLocalServices.addService(CarPowerManagementService.class,
+                mCarPowerManagementServiceMock);
         doReturn(new ArrayList<Integer>()).when(mCarUserServiceMock)
                 .startAllBackgroundUsersInGarageMode();
         doNothing().when(mSystemInterfaceMock)
@@ -122,6 +136,9 @@
         CarLocalServices.addService(CarUserService.class, mCarUserServiceOriginal);
         CarLocalServices.removeServiceForTest(SystemInterface.class);
         CarLocalServices.addService(SystemInterface.class, mSystemInterfaceOriginal);
+        CarLocalServices.removeServiceForTest(CarPowerManagementService.class);
+        CarLocalServices.addService(CarPowerManagementService.class,
+                mCarPowerManagementServiceOriginal);
     }
 
     @Test
@@ -219,4 +236,83 @@
         // Verify that worker that polls running jobs from JobScheduler is scheduled.
         verify(mHandlerMock).postDelayed(any(), eq(JOB_SNAPSHOT_INITIAL_UPDATE_MS));
     }
+
+    @Test
+    public void testInitAndRelease() {
+
+        GarageMode garageMode = mock(GarageMode.class);
+        Controller controller = new Controller(mContextMock, mLooperMock, mWakeupPolicy,
+                mHandlerMock, garageMode);
+
+        controller.init();
+        controller.release();
+
+        verify(garageMode).init();
+        verify(garageMode).release();
+    }
+
+    @Test
+    public void testConstructor() {
+        Resources resourcesMock = mock(Resources.class);
+        when(mContextMock.getResources()).thenReturn(resourcesMock);
+        when(resourcesMock.getStringArray(R.array.config_garageModeCadence))
+                .thenReturn(sTemplateWakeupSchedule);
+        Controller controller = new Controller(mContextMock, mLooperMock);
+
+        assertThat(controller).isNotNull();
+    }
+
+    @Test
+    public void testScheduleNextWakeup() {
+        GarageMode garageMode = mock(GarageMode.class);
+
+        // Enter GarageMode only 1 time, no wake up after that
+        WakeupPolicy wakeUpPolicy = new WakeupPolicy(new String[] { "15m,1" });
+
+        Controller controller = new Controller(mContextMock, mLooperMock, wakeUpPolicy,
+                mHandlerMock, garageMode);
+        controller.setCarPowerManager(mCarPowerManagerMock);
+
+        // Imitate entering and leavimg GarageMode
+        controller.initiateGarageMode(/* future= */ null);
+
+        controller.scheduleNextWakeup();
+
+        verify(mCarPowerManagerMock).scheduleNextWakeupTime(900);
+
+        // Imitate entering Garage mode after sleep
+        controller.initiateGarageMode(/* future= */ null);
+
+        // Should be no more calls to scheduleNextWakeupTime
+        controller.scheduleNextWakeup();
+
+        Mockito.verifyNoMoreInteractions(mCarPowerManagerMock);
+    }
+
+    @Test
+    public void testOnStateChanged() {
+        GarageMode garageMode = mock(GarageMode.class);
+
+        Controller controller = Mockito.spy(new Controller(mContextMock, mLooperMock, mWakeupPolicy,
+                mHandlerMock, garageMode));
+
+        controller.onStateChanged(CarPowerStateListener.SHUTDOWN_CANCELLED, null);
+        verify(controller).resetGarageMode();
+
+        clearInvocations(controller);
+        controller.onStateChanged(CarPowerStateListener.SHUTDOWN_ENTER, null);
+        verify(controller).resetGarageMode();
+
+        clearInvocations(controller);
+        controller.onStateChanged(CarPowerStateListener.SUSPEND_ENTER, null);
+        verify(controller).resetGarageMode();
+
+        clearInvocations(controller);
+        controller.onStateChanged(CarPowerStateListener.SUSPEND_EXIT, null);
+        verify(controller).resetGarageMode();
+
+        clearInvocations(controller);
+        controller.onStateChanged(CarPowerStateListener.INVALID , null);
+        verify(controller, never()).resetGarageMode();
+    }
 }
diff --git a/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java b/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java
index 6133baf..6183549 100644
--- a/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java
+++ b/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -36,6 +37,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.car.CarLocalServices;
+import com.android.car.power.CarPowerManagementService;
 import com.android.car.user.CarUserService;
 
 import org.junit.After;
@@ -50,6 +52,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -131,6 +134,42 @@
                 105);
     }
 
+    @Test
+    public void garageModeTestExitImmediately() throws Exception {
+        CarPowerManagementService mockCarPowerManagementService =
+                mock(CarPowerManagementService.class);
+
+        // Mock CPMS to force Garage Mode early exit
+        CarLocalServices.removeServiceForTest(CarPowerManagementService.class);
+        CarLocalServices.addService(CarPowerManagementService.class, mockCarPowerManagementService);
+        when(mockCarPowerManagementService.garageModeShouldExitImmediately()).thenReturn(true);
+
+        // Check exit immediately without future
+        GarageMode garageMode = new GarageMode(mController);
+        garageMode.init();
+        garageMode.enterGarageMode(/* future= */ null);
+        assertThat(garageMode.isGarageModeActive()).isFalse();
+
+        // Create new instance of GarageMode
+        garageMode = new GarageMode(mController);
+        garageMode.init();
+        // Check exit immediately with future
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        garageMode.enterGarageMode(future);
+        assertThat(garageMode.isGarageModeActive()).isFalse();
+        assertThat(future.isDone()).isTrue();
+
+        // Create new instance of GarageMode
+        garageMode = new GarageMode(mController);
+        garageMode.init();
+        // Check exit immediately with completed future
+        garageMode.enterGarageMode(future);
+        assertThat(garageMode.isGarageModeActive()).isFalse();
+        assertThat(future.isDone()).isTrue();
+
+        CarLocalServices.removeServiceForTest(CarPowerManagementService.class);
+    }
+
     private void waitForHandlerThreadToFinish(CountDownLatch latch) throws Exception {
         assertWithMessage("Latch has timed out.")
                 .that(latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
diff --git a/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java b/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
index 5cb55c3..af14bbf 100644
--- a/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
+++ b/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
@@ -20,6 +20,7 @@
 import static android.hardware.automotive.vehicle.V2_0.CustomInputType.CUSTOM_EVENT_F1;
 
 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.spy;
@@ -74,10 +75,16 @@
 
     private final class CaptureCallback implements CarInputManager.CarInputCaptureCallback {
 
-        private static final long EVENT_WAIT_TIME = 500;
+        private static final long EVENT_WAIT_TIME = 5_000;
 
         private final Object mLock = new Object();
 
+        private final String mName;
+
+        private CaptureCallback(String name) {
+            mName = name;
+        }
+
         // Stores passed events. Last one in front
         @GuardedBy("mLock")
         private final LinkedList<Pair<Integer, List<KeyEvent>>> mKeyEvents = new LinkedList<>();
@@ -149,19 +156,24 @@
         }
 
         private void waitForStateChange() throws Exception {
-            mStateChangeWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+            assertWithMessage("Failed to acquire semaphore in %s ms", EVENT_WAIT_TIME).that(
+                    mStateChangeWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS)).isTrue();
         }
 
         private void waitForKeyEvent() throws Exception {
-            mKeyEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+            assertWithMessage("Failed to acquire semaphore in %s ms", EVENT_WAIT_TIME).that(
+                    mKeyEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS)).isTrue();
         }
 
         private void waitForRotaryEvent() throws Exception {
-            mRotaryEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+            assertWithMessage("Failed to acquire semaphore in %s ms", EVENT_WAIT_TIME).that(
+                    mRotaryEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS)).isTrue();
         }
 
         private void waitForCustomInputEvent() throws Exception {
-            mCustomInputEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+            assertWithMessage("Failed to acquire semaphore in %s ms", EVENT_WAIT_TIME).that(
+                    mCustomInputEventWait.tryAcquire(
+                            EVENT_WAIT_TIME, TimeUnit.MILLISECONDS)).isTrue();
         }
 
         private LinkedList<Pair<Integer, List<KeyEvent>>> getkeyEvents() {
@@ -196,11 +208,16 @@
                 return r;
             }
         }
+
+        @Override
+        public String toString() {
+            return "CaptureCallback{mName='" + mName + "'}";
+        }
     }
 
-    private final CaptureCallback mCallback0 = new CaptureCallback();
-    private final CaptureCallback mCallback1 = new CaptureCallback();
-    private final CaptureCallback mCallback2 = new CaptureCallback();
+    private final CaptureCallback mCallback0 = new CaptureCallback("callback0");
+    private final CaptureCallback mCallback1 = new CaptureCallback("callback1");
+    private final CaptureCallback mCallback2 = new CaptureCallback("callback2");
 
     @Override
     protected synchronized void configureMockedHal() {
@@ -493,7 +510,7 @@
                         CarInputManager.INPUT_TYPE_NAVIGATE_KEYS}, 0, mCallback1);
         assertThat(r).isEqualTo(CarInputManager.INPUT_CAPTURE_RESPONSE_SUCCEEDED);
 
-        mCallback1.waitForStateChange();
+        mCallback0.waitForStateChange();
         assertLastStateChange(CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
                 new int[]{CarInputManager.INPUT_TYPE_DPAD_KEYS},
                 mCallback0);
@@ -539,7 +556,6 @@
         CarInputManager carInputManager1 = createAnotherCarInputManager();
 
         Log.i(TAG, "requestInputEventCapture callback 0");
-
         int r = carInputManager0.requestInputEventCapture(
                 CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
                 new int[]{
@@ -675,7 +691,7 @@
         injectKeyEvent(true, KeyEvent.KEYCODE_NAVIGATE_NEXT);
 
         // Assert: ensure KeyEvent was delivered
-        mCallback1.waitForKeyEvent();
+        mCallback0.waitForKeyEvent();
         assertLastKeyEvent(CarOccupantZoneManager.DISPLAY_TYPE_MAIN, true,
                 KeyEvent.KEYCODE_NAVIGATE_NEXT, mCallback0);
 
@@ -714,7 +730,7 @@
      * Events dispatched to main, so this should guarantee that all event dispatched are completed.
      */
     private void waitForDispatchToMain() {
-        // Needs to twice as it is dispatched to main inside car service once and it is
+        // Needs to be invoked twice as it is dispatched to main inside car service once and it is
         // dispatched to main inside CarInputManager once.
         CarServiceUtils.runOnMainSync(() -> {});
         CarServiceUtils.runOnMainSync(() -> {});
diff --git a/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java b/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java
index 69632f0..b8c4eab 100644
--- a/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java
+++ b/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java
@@ -16,12 +16,18 @@
 
 package com.android.car.pm;
 
+import static androidx.car.app.activity.CarAppActivity.ACTION_SHOW_DIALOG;
+import static androidx.car.app.activity.CarAppActivity.ACTION_START_SECOND_INSTANCE;
+import static androidx.car.app.activity.CarAppActivity.SECOND_INSTANCE_TITLE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertNotNull;
 
 import android.app.Activity;
 import android.app.ActivityOptions;
+import android.app.AlertDialog;
+import android.app.UiAutomation;
 import android.car.Car;
 import android.car.drivingstate.CarDrivingStateEvent;
 import android.car.drivingstate.CarDrivingStateManager;
@@ -30,10 +36,12 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Configurator;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
 import android.view.Display;
 
+import androidx.car.app.activity.CarAppActivity;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
@@ -72,6 +80,8 @@
                 car.getCarManager(Car.CAR_DRIVING_STATE_SERVICE);
         assertNotNull(mCarDrivingStateManager);
 
+        Configurator.getInstance()
+                .setUiAutomationFlags(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
         mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
 
         setDrivingStateMoving();
@@ -98,6 +108,58 @@
     }
 
     @Test
+    public void testBlockingActivity_doActivity_showingDialog_isNotBlocked() throws Exception {
+        Intent intent = new Intent();
+        intent.putExtra(DoActivity.INTENT_EXTRA_SHOW_DIALOG, true);
+        intent.setComponent(toComponentName(getTestContext(), DoActivity.class));
+        startActivity(intent);
+
+        assertThat(mDevice.wait(Until.findObject(By.text(
+                DoActivity.DIALOG_TITLE)),
+                UI_TIMEOUT_MS)).isNotNull();
+        assertBlockingActivityNotFound();
+    }
+
+    @Test
+    public void testBlockingActivity_doTemplateActivity_isNotBlocked() throws Exception {
+        startActivity(toComponentName(getTestContext(), CarAppActivity.class));
+
+        assertThat(mDevice.wait(Until.findObject(By.text(
+                CarAppActivity.class.getSimpleName())),
+                UI_TIMEOUT_MS)).isNotNull();
+        assertBlockingActivityNotFound();
+    }
+
+    @Test
+    public void testBlockingActivity_multipleDoTemplateActivity_notBlocked() throws Exception {
+        startActivity(toComponentName(getTestContext(), CarAppActivity.class));
+        assertThat(mDevice.wait(Until.findObject(By.text(
+                CarAppActivity.class.getSimpleName())),
+                UI_TIMEOUT_MS)).isNotNull();
+        getContext().sendBroadcast(new Intent().setAction(ACTION_START_SECOND_INSTANCE));
+        assertThat(mDevice.wait(Until.findObject(By.text(
+                SECOND_INSTANCE_TITLE)),
+                UI_TIMEOUT_MS)).isNotNull();
+        assertBlockingActivityNotFound();
+    }
+
+    @Test
+    public void testBlockingActivity_doTemplateActivity_showingDialog_isBlocked() throws Exception {
+        startActivity(toComponentName(getTestContext(), CarAppActivity.class));
+        assertThat(mDevice.wait(Until.findObject(By.text(
+                CarAppActivity.class.getSimpleName())),
+                UI_TIMEOUT_MS)).isNotNull();
+        assertBlockingActivityNotFound();
+
+        getContext().sendBroadcast(new Intent().setAction(ACTION_SHOW_DIALOG));
+        assertThat(mDevice.wait(Until.findObject(By.text(DoActivity.DIALOG_TITLE)),
+                UI_TIMEOUT_MS)).isNotNull();
+
+        assertThat(mDevice.wait(Until.findObject(By.res(ACTIVITY_BLOCKING_ACTIVITY_TEXTVIEW_ID)),
+                UI_TIMEOUT_MS)).isNotNull();
+    }
+
+    @Test
     public void testBlockingActivity_nonDoActivity_isBlocked() throws Exception {
         startNonDoActivity(NonDoActivity.EXTRA_DO_NOTHING);
 
@@ -153,11 +215,13 @@
     private void startActivity(ComponentName name) {
         Intent intent = new Intent();
         intent.setComponent(name);
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(intent);
+    }
 
+    private void startActivity(Intent intent) {
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchDisplayId(Display.DEFAULT_DISPLAY);
-
         getContext().startActivity(intent, options.toBundle());
     }
 
@@ -240,6 +304,20 @@
     }
 
     public static class DoActivity extends TempActivity {
+        public static final String INTENT_EXTRA_SHOW_DIALOG = "SHOW_DIALOG";
+        public static final String DIALOG_TITLE = "Title";
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (getIntent().getBooleanExtra(INTENT_EXTRA_SHOW_DIALOG, false)) {
+                AlertDialog dialog = new AlertDialog.Builder(DoActivity.this)
+                        .setTitle(DIALOG_TITLE)
+                        .setMessage("Message")
+                        .create();
+                dialog.show();
+            }
+        }
     }
 
     /** Activity that closes itself after some timeout to clean up the screen. */
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/Android.bp b/tests/carservice_unit_test/Android.bp
index 1f22098..57e14d9 100644
--- a/tests/carservice_unit_test/Android.bp
+++ b/tests/carservice_unit_test/Android.bp
@@ -69,8 +69,6 @@
     // mockito-target-inline dependency
     jni_libs: [
         "libdexmakerjvmtiagent",
-	"libscriptexecutorjni",
-        "libscriptexecutorjniutils-test",
         "libstaticjvmtiagent",
     ],
 }
diff --git a/tests/carservice_unit_test/src/android/car/test/mocks/AndroidMockitoHelperTest.java b/tests/carservice_unit_test/src/android/car/test/mocks/AndroidMockitoHelperTest.java
index e1099da..b495220 100644
--- a/tests/carservice_unit_test/src/android/car/test/mocks/AndroidMockitoHelperTest.java
+++ b/tests/carservice_unit_test/src/android/car/test/mocks/AndroidMockitoHelperTest.java
@@ -24,6 +24,7 @@
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmCreateUser;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetAliveUsers;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetSystemUser;
+import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUserHandles;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUserInfo;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmIsHeadlessSystemUserMode;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmIsUserRunning;
@@ -160,6 +161,16 @@
     }
 
     @Test
+    public void testMockUmGetUserHandles() {
+        UserHandle user1 = UserHandle.of(100);
+        UserHandle user2 = UserHandle.of(200);
+
+        mockUmGetUserHandles(mMockedUserManager, true, 100, 200);
+
+        assertThat(mMockedUserManager.getUserHandles(true)).containsExactly(user1, user2).inOrder();
+    }
+
+    @Test
     public void testMockBinderGetCallingUserHandle() {
         mockBinderGetCallingUserHandle(TEST_USER_ID);
 
diff --git a/tests/carservice_unit_test/src/android/car/watchdoglib/CarWatchdogDaemonHelperTest.java b/tests/carservice_unit_test/src/android/car/watchdoglib/CarWatchdogDaemonHelperTest.java
index c67d3d8..99bbc4c 100644
--- a/tests/carservice_unit_test/src/android/car/watchdoglib/CarWatchdogDaemonHelperTest.java
+++ b/tests/carservice_unit_test/src/android/car/watchdoglib/CarWatchdogDaemonHelperTest.java
@@ -193,6 +193,13 @@
         verify(mFakeCarWatchdog).actionTakenOnResourceOveruse(eq(actions));
     }
 
+    @Test
+    public void testIndirectCall_controlProcessHealthCheck() throws Exception {
+        mCarWatchdogDaemonHelper.controlProcessHealthCheck(true);
+
+        verify(mFakeCarWatchdog).controlProcessHealthCheck(eq(true));
+    }
+
     /*
      * Test that the {@link CarWatchdogDaemonHelper} throws {@code IllegalArgumentException} when
      * trying to register already-registered service again.
diff --git a/tests/carservice_unit_test/src/com/android/car/BluetoothFastPairTest.java b/tests/carservice_unit_test/src/com/android/car/BluetoothFastPairTest.java
index d7a8f3b..ebc9c2e 100644
--- a/tests/carservice_unit_test/src/com/android/car/BluetoothFastPairTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/BluetoothFastPairTest.java
@@ -24,7 +24,9 @@
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.bluetooth.BluetoothAdapter;
@@ -120,6 +122,7 @@
     static final int TEST_PIN_NUMBER = 66051;
     static final int TEST_MODEL_ID = 4386;
     static final byte[] TEST_MODEL_ID_BYTES = {0x00, 0x11, 0x22};
+    static final int ASYNC_CALL_TIMEOUT_MILLIS = 200;
     byte[] mAdvertisementExpectedResults = new byte[]{0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00,
             0x11, 0x00};
     @Mock
@@ -339,12 +342,27 @@
         assertThat(mTestGattServer.validatePairingRequest(encryptedRequest,
                 testKey.getKeySpec())).isTrue();
         //send Wrong Pairing Key
-        sendPairingKey(-1);
+        sendPairingKey(-2);
         mTestGattServer.processPairingKey(encryptedPairingKey);
         verify(mMockBluetoothDevice).setPairingConfirmation(false);
     }
 
     @Test
+    public void testNoPairingKey() {
+        FastPairUtils.AccountKey testKey = new FastPairUtils.AccountKey(TEST_SHARED_SECRET);
+        mTestGattServer.setSharedSecretKey(testKey.toBytes());
+        byte[] encryptedRequest = mTestGattServer.encrypt(TEST_PAIRING_REQUEST);
+
+        byte[] encryptedPairingKey = mTestGattServer.encrypt(TEST_PAIRING_KEY);
+
+        assertThat(mTestGattServer.validatePairingRequest(encryptedRequest,
+                testKey.getKeySpec())).isTrue();
+        mTestGattServer.processPairingKey(encryptedPairingKey);
+        verifyNoMoreInteractions(mMockBluetoothDevice);
+    }
+
+
+    @Test
     public void testValidPairingKeyAutoAccept() {
         FastPairUtils.AccountKey testKey = new FastPairUtils.AccountKey(TEST_SHARED_SECRET);
         mTestGattServer.setSharedSecretKey(testKey.toBytes());
@@ -440,10 +458,27 @@
                 .isEqualTo(TEST_MODEL_ID_BYTES);
     }
 
+    @Test
+    public void testStopAdvertisements() {
+        mTestFastPairProvider.start();
+        when(mMockBluetoothAdapter.isDiscovering()).thenReturn(true);
+        Intent scanMode = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+        scanMode.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+        mTestFastPairProvider.mDiscoveryModeChanged.onReceive(mMockContext, scanMode);
+
+        when(mMockBluetoothAdapter.isDiscovering()).thenReturn(false);
+        scanMode.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+                BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+        mTestFastPairProvider.mDiscoveryModeChanged.onReceive(mMockContext, scanMode);
+        verify(mMockLeAdvertiser, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).stopAdvertisingSet(any());
+    }
+
     void sendPairingKey(int pairingKey) {
         Intent pairingRequest = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST);
         pairingRequest.putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, pairingKey);
         pairingRequest.putExtra(BluetoothDevice.EXTRA_DEVICE, mMockBluetoothDevice);
         mTestGattServer.mPairingAttemptsReceiver.onReceive(mMockContext, pairingRequest);
+
     }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/CarInputRotaryServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarInputRotaryServiceTest.java
index b832462..acfa606 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarInputRotaryServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarInputRotaryServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.car;
 
+import static com.android.car.CarInputService.ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -48,6 +50,7 @@
 import com.android.car.hal.InputHalService;
 import com.android.car.hal.UserHalService;
 import com.android.car.internal.common.CommonConstants.UserLifecycleEventType;
+import com.android.car.pm.CarSafetyAccessibilityService;
 import com.android.car.user.CarUserService;
 import com.android.internal.app.AssistUtils;
 import com.android.internal.util.test.BroadcastInterceptingContext;
@@ -140,12 +143,21 @@
     }
 
     @Test
-    public void rotaryServiceSettingsUpdated_whenRotaryServiceIsNotEmpty() throws Exception {
+    public void accessibilitySettingsUpdated_whenRotaryServiceIsNotEmpty() throws Exception {
+        final String existingService = "com.android.temp/com.android.car.TempService";
         final String rotaryService = "com.android.car.rotary/com.android.car.rotary.RotaryService";
+        final String carSafetyAccessibilityService = mContext.getPackageName()
+                + "/"
+                + CarSafetyAccessibilityService.class.getName();
         init(rotaryService);
         assertThat(mMockContext.getString(R.string.rotaryService)).isEqualTo(rotaryService);
-
         final int userId = 11;
+        Settings.Secure.putStringForUser(
+                mMockContext.getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                existingService,
+                userId);
+
 
         // By default RotaryService is not enabled.
         String enabledServices = Settings.Secure.getStringForUser(
@@ -167,7 +179,12 @@
                 mMockContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 userId);
-        assertThat(enabledServices).contains(rotaryService);
+        assertThat(enabledServices).isEqualTo(
+                existingService
+                        + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
+                        + carSafetyAccessibilityService
+                        + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
+                        + rotaryService);
 
         enabled = Settings.Secure.getStringForUser(
                 mMockContext.getContentResolver(),
@@ -177,7 +194,11 @@
     }
 
     @Test
-    public void rotaryServiceSettingsNotUpdated_whenRotaryServiceIsEmpty() throws Exception {
+    public void accessibilitySettingsUpdated_withoutRotaryService_whenRotaryServiceIsEmpty()
+            throws Exception {
+        final String carSafetyAccessibilityService = mContext.getPackageName()
+                + "/"
+                + CarSafetyAccessibilityService.class.getName();
         final String rotaryService = "";
         init(rotaryService);
         assertThat(mMockContext.getString(R.string.rotaryService)).isEqualTo(rotaryService);
@@ -199,7 +220,55 @@
                 mMockContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_ENABLED,
                 userId);
+        assertThat(enabled).isEqualTo("1");
+        String enabledServices = Settings.Secure.getStringForUser(
+                mMockContext.getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                userId);
+        assertThat(enabledServices).isEqualTo(carSafetyAccessibilityService);
+    }
+
+    @Test
+    public void accessibilitySettingsUpdated_accessibilityServicesAlreadyEnabled()
+            throws Exception {
+        final String rotaryService = "com.android.car.rotary/com.android.car.rotary.RotaryService";
+        final String carSafetyAccessibilityService = mContext.getPackageName()
+                + "/"
+                + CarSafetyAccessibilityService.class.getName();
+        init(rotaryService);
+        assertThat(mMockContext.getString(R.string.rotaryService)).isEqualTo(rotaryService);
+        final int userId = 11;
+        Settings.Secure.putStringForUser(
+                mMockContext.getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                carSafetyAccessibilityService
+                        + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
+                        + rotaryService,
+                userId);
+
+        String enabled = Settings.Secure.getStringForUser(
+                mMockContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_ENABLED,
+                userId);
         assertThat(enabled).isNull();
+
+        // Enable RotaryService by sending user switch event.
+        sendUserLifecycleEvent(CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING, userId);
+
+        String enabledServices = Settings.Secure.getStringForUser(
+                mMockContext.getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                userId);
+        assertThat(enabledServices).isEqualTo(
+                carSafetyAccessibilityService
+                        + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
+                        + rotaryService);
+
+        enabled = Settings.Secure.getStringForUser(
+                mMockContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_ENABLED,
+                userId);
+        assertThat(enabled).isEqualTo("1");
     }
 
     @After
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPolicyVolumeCallbackTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPolicyVolumeCallbackTest.java
index fc78159..b070bd8 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPolicyVolumeCallbackTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPolicyVolumeCallbackTest.java
@@ -153,6 +153,35 @@
     }
 
     @Test
+    public void onVolumeAdjustment_withAdjustRaise_whileMuted_setsGroupVolumeToMin() {
+        setGroupVolume(TEST_MAX_VOLUME);
+        setGroupVolumeMute(true);
+
+        CarAudioPolicyVolumeCallback callback =
+                new CarAudioPolicyVolumeCallback(mMockCarAudioService, mMockAudioManager, true);
+
+
+        callback.onVolumeAdjustment(ADJUST_RAISE);
+
+        verify(mMockCarAudioService).setGroupVolume(PRIMARY_AUDIO_ZONE,
+                TEST_VOLUME_GROUP, TEST_MIN_VOLUME, TEST_EXPECTED_FLAGS);
+    }
+
+    @Test
+    public void onVolumeAdjustment_withAdjustLower_whileMuted_setsGroupVolumeToMin() {
+        setGroupVolume(TEST_MAX_VOLUME);
+        setGroupVolumeMute(true);
+
+        CarAudioPolicyVolumeCallback callback =
+                new CarAudioPolicyVolumeCallback(mMockCarAudioService, mMockAudioManager, true);
+
+        callback.onVolumeAdjustment(ADJUST_LOWER);
+
+        verify(mMockCarAudioService).setGroupVolume(PRIMARY_AUDIO_ZONE,
+                TEST_VOLUME_GROUP, TEST_MIN_VOLUME, TEST_EXPECTED_FLAGS);
+    }
+
+    @Test
     public void onVolumeAdjustment_withAdjustSame_doesNothing() {
         setGroupVolume(TEST_VOLUME);
 
@@ -207,8 +236,8 @@
 
     @Test
     public void onVolumeAdjustment_forGroupMute_withAdjustToggleMute_togglesMutesVolumeGroup() {
-        when(mMockCarAudioService.isVolumeGroupMuted(anyInt(), anyInt()))
-                .thenReturn(true);
+        setGroupVolumeMute(true);
+
         CarAudioPolicyVolumeCallback callback =
                 new CarAudioPolicyVolumeCallback(mMockCarAudioService, mMockAudioManager, true);
 
@@ -220,7 +249,8 @@
 
     @Test
     public void onVolumeAdjustment_forGroupMute_withAdjustUnMute_UnMutesVolumeGroup() {
-        when(mMockCarAudioService.isVolumeGroupMuted(anyInt(), anyInt())).thenReturn(false);
+        setGroupVolumeMute(false);
+
         CarAudioPolicyVolumeCallback callback =
                 new CarAudioPolicyVolumeCallback(mMockCarAudioService, mMockAudioManager, true);
 
@@ -234,4 +264,9 @@
         when(mMockCarAudioService.getGroupVolume(anyInt(), anyInt()))
                 .thenReturn(groupVolume);
     }
+
+    private void setGroupVolumeMute(boolean mute) {
+        when(mMockCarAudioService.isVolumeGroupMuted(anyInt(), anyInt()))
+                .thenReturn(mute);
+    }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java
index e913b53..b3d02be 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java
@@ -16,14 +16,23 @@
 
 package com.android.car.audio;
 
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_FM_TUNER;
+
 import static com.android.car.audio.CarAudioUtils.hasExpired;
+import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.when;
+
+import android.media.AudioDeviceInfo;
+
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 
 @RunWith(AndroidJUnit4.class)
 public class CarAudioUtilsTest {
@@ -37,4 +46,18 @@
     public void hasExpired_forCurrentTimeAfterTimeout_returnsFalse() {
         assertThat(hasExpired(0, 300, 200)).isTrue();
     }
+
+    @Test
+    public void isMicrophoneInputDevice_forMicrophoneDevice_returnsTrue() {
+        AudioDeviceInfo deviceInfo = Mockito.mock(AudioDeviceInfo.class);
+        when(deviceInfo.getType()).thenReturn(TYPE_BUILTIN_MIC);
+        assertThat(isMicrophoneInputDevice(deviceInfo)).isTrue();
+    }
+
+    @Test
+    public void isMicrophoneInputDevice_forNonMicrophoneDevice_returnsFalse() {
+        AudioDeviceInfo deviceInfo = Mockito.mock(AudioDeviceInfo.class);
+        when(deviceInfo.getType()).thenReturn(TYPE_FM_TUNER);
+        assertThat(isMicrophoneInputDevice(deviceInfo)).isFalse();
+    }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingUtilsTest.java
index de39471..ece0f00 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingUtilsTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingUtilsTest.java
@@ -23,8 +23,15 @@
 import static android.media.AudioAttributes.USAGE_MEDIA;
 import static android.media.AudioAttributes.USAGE_NOTIFICATION;
 import static android.media.AudioAttributes.USAGE_SAFETY;
+import static android.media.AudioAttributes.USAGE_VIRTUAL_SOURCE;
 import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
 
+import static com.android.car.audio.CarAudioContext.CALL;
+import static com.android.car.audio.CarAudioContext.EMERGENCY;
+import static com.android.car.audio.CarAudioContext.INVALID;
+import static com.android.car.audio.CarAudioContext.MUSIC;
+import static com.android.car.audio.CarAudioContext.NAVIGATION;
+
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -161,6 +168,16 @@
     }
 
     @Test
+    public void getAddressesToDuck_doesNotConsidersInvalidUsage() {
+        CarAudioZone mockZone = generateAudioZoneMock();
+        int[] usages = new int[]{USAGE_VIRTUAL_SOURCE};
+
+        List<String> addresses = CarDuckingUtils.getAddressesToDuck(usages, mockZone);
+
+        assertThat(addresses).isEmpty();
+    }
+
+    @Test
     public void getAddressesToDuck_withDuckedAndUnduckedContextsSharingDevice_excludesThatDevice() {
         CarAudioZone mockZone = generateAudioZoneMock();
         when(mockZone.getAddressForContext(CarAudioContext.SAFETY)).thenReturn(NAVIGATION_ADDRESS);
@@ -220,12 +237,11 @@
 
     private static CarAudioZone generateAudioZoneMock() {
         CarAudioZone mockZone = mock(CarAudioZone.class);
-        when(mockZone.getAddressForContext(CarAudioContext.MUSIC)).thenReturn(MEDIA_ADDRESS);
-        when(mockZone.getAddressForContext(CarAudioContext.EMERGENCY)).thenReturn(
-                EMERGENCY_ADDRESS);
-        when(mockZone.getAddressForContext(CarAudioContext.CALL)).thenReturn(CALL_ADDRESS);
-        when(mockZone.getAddressForContext(CarAudioContext.NAVIGATION)).thenReturn(
-                NAVIGATION_ADDRESS);
+        when(mockZone.getAddressForContext(MUSIC)).thenReturn(MEDIA_ADDRESS);
+        when(mockZone.getAddressForContext(EMERGENCY)).thenReturn(EMERGENCY_ADDRESS);
+        when(mockZone.getAddressForContext(CALL)).thenReturn(CALL_ADDRESS);
+        when(mockZone.getAddressForContext(NAVIGATION)).thenReturn(NAVIGATION_ADDRESS);
+        when(mockZone.getAddressForContext(INVALID)).thenThrow(new IllegalArgumentException());
 
         return mockZone;
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java
index 9e38583..d71a0d5 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java
@@ -16,7 +16,15 @@
 
 package com.android.car.audio;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.android.car.audio.CarAudioContext.ALARM;
+import static com.android.car.audio.CarAudioContext.CALL;
+import static com.android.car.audio.CarAudioContext.CALL_RING;
+import static com.android.car.audio.CarAudioContext.EMERGENCY;
+import static com.android.car.audio.CarAudioContext.MUSIC;
+import static com.android.car.audio.CarAudioContext.NAVIGATION;
+import static com.android.car.audio.CarAudioContext.NOTIFICATION;
+
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -70,11 +78,12 @@
     public void setDeviceInfoForContext_associatesDeviceAddresses() {
         CarVolumeGroup.Builder builder = getBuilder();
 
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, mNavigationDeviceInfo);
         CarVolumeGroup carVolumeGroup = builder.build();
 
-        assertThat(carVolumeGroup.getAddresses()).containsExactly(MEDIA_DEVICE_ADDRESS,
+        assertWithMessage("%s and %s", MEDIA_DEVICE_ADDRESS, NAVIGATION_DEVICE_ADDRESS)
+                .that(carVolumeGroup.getAddresses()).containsExactly(MEDIA_DEVICE_ADDRESS,
                 NAVIGATION_DEVICE_ADDRESS);
     }
 
@@ -82,139 +91,149 @@
     public void setDeviceInfoForContext_associatesContexts() {
         CarVolumeGroup.Builder builder = getBuilder();
 
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, mNavigationDeviceInfo);
         CarVolumeGroup carVolumeGroup = builder.build();
 
-        assertThat(carVolumeGroup.getContexts()).asList().containsExactly(CarAudioContext.MUSIC,
-                CarAudioContext.NAVIGATION);
+        assertWithMessage("Music[%s] and Navigation[%s] Context", MUSIC, NAVIGATION)
+                .that(carVolumeGroup.getContexts()).asList().containsExactly(MUSIC, NAVIGATION);
     }
 
     @Test
     public void setDeviceInfoForContext_withDifferentStepSize_throws() {
         CarVolumeGroup.Builder builder = getBuilder();
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
         CarAudioDeviceInfo differentStepValueDevice = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
                 .setStepValue(mMediaDeviceInfo.getStepValue() + 1).build();
 
         IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
-                () -> builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION,
+                () -> builder.setDeviceInfoForContext(NAVIGATION,
                         differentStepValueDevice));
 
-        assertThat(thrown).hasMessageThat()
+        assertWithMessage("setDeviceInfoForContext failure for different step size")
+                .that(thrown).hasMessageThat()
                 .contains("Gain controls within one group must have same step value");
     }
 
     @Test
     public void setDeviceInfoForContext_withSameContext_throws() {
         CarVolumeGroup.Builder builder = getBuilder();
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
 
         IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
-                () -> builder.setDeviceInfoForContext(CarAudioContext.MUSIC,
+                () -> builder.setDeviceInfoForContext(MUSIC,
                         mNavigationDeviceInfo));
 
-        assertThat(thrown).hasMessageThat()
-                .contains("has already been set to");
+        assertWithMessage("setDeviceInfoForSameContext failure for repeated context")
+                .that(thrown).hasMessageThat().contains("has already been set to");
     }
 
     @Test
     public void setDeviceInfoForContext_withFirstCall_setsMinGain() {
         CarVolumeGroup.Builder builder = getBuilder();
 
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
 
-        assertThat(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
+        assertWithMessage("Min Gain from builder")
+                .that(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
     }
 
     @Test
     public void setDeviceInfoForContext_withFirstCall_setsMaxGain() {
         CarVolumeGroup.Builder builder = getBuilder();
 
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
 
-        assertThat(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
+        assertWithMessage("Max Gain from builder")
+                .that(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
     }
 
     @Test
     public void setDeviceInfoForContext_withFirstCall_setsDefaultGain() {
         CarVolumeGroup.Builder builder = getBuilder();
 
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
 
-        assertThat(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
+        assertWithMessage("Default Gain from builder")
+                .that(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
     }
 
     @Test
     public void setDeviceInfoForContext_SecondCallWithSmallerMinGain_updatesMinGain() {
         CarVolumeGroup.Builder builder = getBuilder();
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
         CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
                 .setMinGain(mMediaDeviceInfo.getMinGain() - 1).build();
 
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
 
-        assertThat(builder.mMinGain).isEqualTo(secondInfo.getMinGain());
+        assertWithMessage("Second, smaller min gain from builder")
+                .that(builder.mMinGain).isEqualTo(secondInfo.getMinGain());
     }
 
     @Test
     public void setDeviceInfoForContext_SecondCallWithLargerMinGain_keepsFirstMinGain() {
         CarVolumeGroup.Builder builder = getBuilder();
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
         CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
                 .setMinGain(mMediaDeviceInfo.getMinGain() + 1).build();
 
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
 
-        assertThat(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
+        assertWithMessage("First, smaller min gain from builder")
+                .that(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
     }
 
     @Test
     public void setDeviceInfoForContext_SecondCallWithLargerMaxGain_updatesMaxGain() {
         CarVolumeGroup.Builder builder = getBuilder();
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
         CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
                 .setMaxGain(mMediaDeviceInfo.getMaxGain() + 1).build();
 
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
 
-        assertThat(builder.mMaxGain).isEqualTo(secondInfo.getMaxGain());
+        assertWithMessage("Second, larger max gain from builder")
+                .that(builder.mMaxGain).isEqualTo(secondInfo.getMaxGain());
     }
 
     @Test
     public void setDeviceInfoForContext_SecondCallWithSmallerMaxGain_keepsFirstMaxGain() {
         CarVolumeGroup.Builder builder = getBuilder();
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
         CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
                 .setMaxGain(mMediaDeviceInfo.getMaxGain() - 1).build();
 
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
 
-        assertThat(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
+        assertWithMessage("First, larger max gain from builder")
+                .that(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
     }
 
     @Test
     public void setDeviceInfoForContext_SecondCallWithLargerDefaultGain_updatesDefaultGain() {
         CarVolumeGroup.Builder builder = getBuilder();
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
         CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
                 .setDefaultGain(mMediaDeviceInfo.getDefaultGain() + 1).build();
 
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
 
-        assertThat(builder.mDefaultGain).isEqualTo(secondInfo.getDefaultGain());
+        assertWithMessage("Second, larger default gain from builder")
+                .that(builder.mDefaultGain).isEqualTo(secondInfo.getDefaultGain());
     }
 
     @Test
     public void setDeviceInfoForContext_SecondCallWithSmallerDefaultGain_keepsFirstDefaultGain() {
         CarVolumeGroup.Builder builder = getBuilder();
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
         CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
                 .setDefaultGain(mMediaDeviceInfo.getDefaultGain() - 1).build();
 
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
 
-        assertThat(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
+        assertWithMessage("Second, smaller default gain from builder")
+                .that(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
     }
 
     @Test
@@ -223,13 +242,14 @@
 
         Exception e = expectThrows(IllegalArgumentException.class, builder::build);
 
-        assertThat(e).hasMessageThat().isEqualTo(
-                "setDeviceInfoForContext has to be called at least once before building");
+        assertWithMessage("Builder build failure").that(e).hasMessageThat()
+                .isEqualTo(
+                        "setDeviceInfoForContext has to be called at least once before building");
     }
 
     @Test
     public void builderBuild_withNoStoredGain_usesDefaultGain() {
-        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(MUSIC,
                 mMediaDeviceInfo);
         when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
                 GROUP_ID)).thenReturn(-1);
@@ -237,50 +257,55 @@
 
         CarVolumeGroup carVolumeGroup = builder.build();
 
-        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+        assertWithMessage("Current gain index")
+                .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
     }
 
     @Test
     public void builderBuild_withTooLargeStoredGain_usesDefaultGain() {
-        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(MUSIC,
                 mMediaDeviceInfo);
         when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
                 GROUP_ID)).thenReturn(MAX_GAIN_INDEX + 1);
 
         CarVolumeGroup carVolumeGroup = builder.build();
 
-        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+        assertWithMessage("Current gain index")
+                .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
     }
 
     @Test
     public void builderBuild_withTooSmallStoredGain_usesDefaultGain() {
-        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(MUSIC,
                 mMediaDeviceInfo);
         when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
                 GROUP_ID)).thenReturn(MIN_GAIN_INDEX - 1);
 
         CarVolumeGroup carVolumeGroup = builder.build();
 
-        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+        assertWithMessage("Current gain index")
+                .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
     }
 
     @Test
     public void builderBuild_withValidStoredGain_usesStoredGain() {
-        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+        CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(MUSIC,
                 mMediaDeviceInfo);
         when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
                 GROUP_ID)).thenReturn(MAX_GAIN_INDEX - 1);
 
         CarVolumeGroup carVolumeGroup = builder.build();
 
-        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(MAX_GAIN_INDEX - 1);
+        assertWithMessage("Current gain index")
+                .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(MAX_GAIN_INDEX - 1);
     }
 
     @Test
     public void getAddressForContext_withSupportedContext_returnsAddress() {
         CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
 
-        assertThat(carVolumeGroup.getAddressForContext(CarAudioContext.MUSIC))
+        assertWithMessage("Supported context's address")
+                .that(carVolumeGroup.getAddressForContext(MUSIC))
                 .isEqualTo(mMediaDeviceInfo.getAddress());
     }
 
@@ -288,14 +313,16 @@
     public void getAddressForContext_withUnsupportedContext_returnsNull() {
         CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
 
-        assertThat(carVolumeGroup.getAddressForContext(CarAudioContext.NAVIGATION)).isNull();
+        assertWithMessage("Unsupported context's address")
+                .that(carVolumeGroup.getAddressForContext(NAVIGATION)).isNull();
     }
 
     @Test
     public void isMuted_whenDefault_returnsFalse() {
         CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
 
-        assertThat(carVolumeGroup.isMuted()).isFalse();
+        assertWithMessage("Default mute state")
+                .that(carVolumeGroup.isMuted()).isFalse();
     }
 
     @Test
@@ -304,7 +331,8 @@
 
         carVolumeGroup.setMute(true);
 
-        assertThat(carVolumeGroup.isMuted()).isTrue();
+        assertWithMessage("Set mute state")
+                .that(carVolumeGroup.isMuted()).isTrue();
     }
 
     @Test
@@ -313,7 +341,8 @@
 
         carVolumeGroup.setMute(false);
 
-        assertThat(carVolumeGroup.isMuted()).isFalse();
+        assertWithMessage("Set mute state")
+                .that(carVolumeGroup.isMuted()).isFalse();
     }
 
     @Test
@@ -352,8 +381,9 @@
 
         List<Integer> contextsList = carVolumeGroup.getContextsForAddress(MEDIA_DEVICE_ADDRESS);
 
-        assertThat(contextsList).containsExactly(CarAudioContext.MUSIC,
-                CarAudioContext.CALL, CarAudioContext.CALL_RING);
+        assertWithMessage("Contexts for bounded address %s", MEDIA_DEVICE_ADDRESS)
+                .that(contextsList).containsExactly(MUSIC,
+                CALL, CALL_RING);
     }
 
     @Test
@@ -362,7 +392,8 @@
 
         List<Integer> contextsList = carVolumeGroup.getContextsForAddress(OTHER_ADDRESS);
 
-        assertThat(contextsList).isEmpty();
+        assertWithMessage("Contexts for non-bounded address %s", OTHER_ADDRESS)
+                .that(contextsList).isEmpty();
     }
 
     @Test
@@ -372,7 +403,8 @@
         CarAudioDeviceInfo actualDevice = carVolumeGroup.getCarAudioDeviceInfoForAddress(
                 MEDIA_DEVICE_ADDRESS);
 
-        assertThat(actualDevice).isEqualTo(mMediaDeviceInfo);
+        assertWithMessage("Device information for bounded address %s", MEDIA_DEVICE_ADDRESS)
+                .that(actualDevice).isEqualTo(mMediaDeviceInfo);
     }
 
     @Test
@@ -382,7 +414,8 @@
         CarAudioDeviceInfo actualDevice = carVolumeGroup.getCarAudioDeviceInfoForAddress(
                 OTHER_ADDRESS);
 
-        assertThat(actualDevice).isNull();
+        assertWithMessage("Device information for non-bounded address %s", OTHER_ADDRESS)
+                .that(actualDevice).isNull();
     }
 
     @Test
@@ -401,7 +434,8 @@
 
         carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
 
-        assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(TEST_GAIN_INDEX);
+        assertWithMessage("Updated current gain index")
+                .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(TEST_GAIN_INDEX);
     }
 
     @Test
@@ -410,7 +444,8 @@
 
         IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
                 () -> carVolumeGroup.setCurrentGainIndex(MIN_GAIN_INDEX - 1));
-        assertThat(thrown).hasMessageThat()
+        assertWithMessage("Set out of bound gain index failure")
+                .that(thrown).hasMessageThat()
                 .contains("Gain out of range (" + MIN_GAIN + ":" + MAX_GAIN + ")");
     }
 
@@ -420,7 +455,8 @@
 
         IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
                 () -> carVolumeGroup.setCurrentGainIndex(MAX_GAIN_INDEX + 1));
-        assertThat(thrown).hasMessageThat()
+        assertWithMessage("Set out of bound gain index failure")
+                .that(thrown).hasMessageThat()
                 .contains("Gain out of range (" + MIN_GAIN + ":" + MAX_GAIN + ")");
     }
 
@@ -456,7 +492,8 @@
 
         carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
 
-        assertThat(carVolumeGroup.isMuted()).isTrue();
+        assertWithMessage("Saved mute state from settings")
+                .that(carVolumeGroup.isMuted()).isTrue();
     }
 
     @Test
@@ -465,7 +502,8 @@
 
         carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
 
-        assertThat(carVolumeGroup.isMuted()).isFalse();
+        assertWithMessage("Default mute state")
+                .that(carVolumeGroup.isMuted()).isFalse();
     }
 
     @Test
@@ -474,7 +512,8 @@
 
         carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
 
-        assertThat(carVolumeGroup.isMuted()).isFalse();
+        assertWithMessage("Saved mute state from settings")
+                .that(carVolumeGroup.isMuted()).isFalse();
     }
 
     @Test
@@ -483,35 +522,70 @@
 
         carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
 
-        assertThat(carVolumeGroup.isMuted()).isFalse();
+        assertWithMessage("Default mute state")
+                .that(carVolumeGroup.isMuted()).isFalse();
     }
 
     @Test
     public void hasCriticalAudioContexts_withoutCriticalContexts_returnsFalse() {
         CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
 
-        assertThat(carVolumeGroup.hasCriticalAudioContexts()).isFalse();
+        assertWithMessage("Group without critical audio context")
+                .that(carVolumeGroup.hasCriticalAudioContexts()).isFalse();
     }
 
     @Test
     public void hasCriticalAudioContexts_withCriticalContexts_returnsTrue() {
         CarVolumeGroup carVolumeGroup = getBuilder()
-                .setDeviceInfoForContext(CarAudioContext.EMERGENCY, mMediaDeviceInfo)
+                .setDeviceInfoForContext(EMERGENCY, mMediaDeviceInfo)
                 .build();
 
-        assertThat(carVolumeGroup.hasCriticalAudioContexts()).isTrue();
+        assertWithMessage("Group with critical audio context")
+                .that(carVolumeGroup.hasCriticalAudioContexts()).isTrue();
+    }
+
+    @Test
+    public void getCurrentGainIndex_whileMuted_returnsMinGain() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+        carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
+
+        carVolumeGroup.setMute(true);
+
+        assertWithMessage("Muted current gain index")
+                .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(MIN_GAIN_INDEX);
+    }
+
+    @Test
+    public void getCurrentGainIndex_whileUnMuted_returnsLastSetGain() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+        carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
+
+        carVolumeGroup.setMute(false);
+
+        assertWithMessage("Un-muted current gain index")
+                .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(TEST_GAIN_INDEX);
+    }
+
+    @Test
+    public void setCurrentGainIndex_whileMuted_unMutesVolumeGroup() {
+        CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+        carVolumeGroup.setMute(true);
+        carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
+
+        assertWithMessage("Mute state after volume change")
+                .that(carVolumeGroup.isMuted()).isEqualTo(false);
     }
 
     private CarVolumeGroup getCarVolumeGroupWithMusicBound() {
         return getBuilder()
-                .setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo)
+                .setDeviceInfoForContext(MUSIC, mMediaDeviceInfo)
                 .build();
     }
 
     private CarVolumeGroup getCarVolumeGroupWithNavigationBound(CarAudioSettings settings,
             boolean useCarVolumeGroupMute) {
         return new CarVolumeGroup.Builder(0, 0, settings, useCarVolumeGroupMute)
-                .setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo)
+                .setDeviceInfoForContext(NAVIGATION, mNavigationDeviceInfo)
                 .build();
     }
 
@@ -527,13 +601,13 @@
     private CarVolumeGroup testVolumeGroupSetup() {
         CarVolumeGroup.Builder builder = getBuilder();
 
-        builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
-        builder.setDeviceInfoForContext(CarAudioContext.CALL, mMediaDeviceInfo);
-        builder.setDeviceInfoForContext(CarAudioContext.CALL_RING, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(CALL, mMediaDeviceInfo);
+        builder.setDeviceInfoForContext(CALL_RING, mMediaDeviceInfo);
 
-        builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
-        builder.setDeviceInfoForContext(CarAudioContext.ALARM, mNavigationDeviceInfo);
-        builder.setDeviceInfoForContext(CarAudioContext.NOTIFICATION, mNavigationDeviceInfo);
+        builder.setDeviceInfoForContext(NAVIGATION, mNavigationDeviceInfo);
+        builder.setDeviceInfoForContext(ALARM, mNavigationDeviceInfo);
+        builder.setDeviceInfoForContext(NOTIFICATION, mNavigationDeviceInfo);
 
         return builder.build();
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java b/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
index ff50677..2ab14dc 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
@@ -20,6 +20,8 @@
 import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
 import android.util.Log;
 
+import com.android.car.CarServiceUtils;
+
 import java.util.LinkedList;
 
 public class MockedPowerHalService extends PowerHalService {
@@ -47,7 +49,8 @@
                 mock(UserHalService.class),
                 mock(DiagnosticHalService.class),
                 mock(ClusterHalService.class),
-                mock(HalClient.class));
+                mock(HalClient.class),
+                CarServiceUtils.getHandlerThread(VehicleHal.class.getSimpleName()));
         return mockedVehicleHal;
     }
 
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java b/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
index 763b680..4aa9a81 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
@@ -35,6 +35,10 @@
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyChangeMode;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.car.CarServiceUtils;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -69,6 +73,10 @@
     @Mock private ClusterHalService mClusterHalService;
     @Mock private HalClient mHalClient;
 
+    private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
+            VehicleHal.class.getSimpleName());
+    private final Handler mHandler = new Handler(mHandlerThread.getLooper());
+
     private VehicleHal mVehicleHal;
 
     /** Hal services configurations */
@@ -78,7 +86,7 @@
     public void setUp() throws Exception {
         mVehicleHal = new VehicleHal(mPowerHalService,
                 mPropertyHalService, mInputHalService, mVmsHalService, mUserHalService,
-                mDiagnosticHalService, mClusterHalService, mHalClient);
+                mDiagnosticHalService, mClusterHalService, mHalClient, mHandlerThread);
 
         mConfigs.clear();
 
@@ -185,7 +193,8 @@
         propValues.add(propValue);
 
         // Act
-        mVehicleHal.onPropertyEvent(propValues);
+        mHandler.post(() -> mVehicleHal.onPropertyEvent(propValues));
+        CarServiceUtils.runOnLooperSync(mHandlerThread.getLooper(), () -> {});
 
         // Assert
         verify(dispatchList).add(propValue);
@@ -201,7 +210,8 @@
         int areaId = VehicleHal.NO_AREA;
 
         // Act
-        mVehicleHal.onPropertySetError(errorCode, propId, areaId);
+        mHandler.post(() -> mVehicleHal.onPropertySetError(errorCode, propId, areaId));
+        CarServiceUtils.runOnLooperSync(mHandlerThread.getLooper(), () -> {});
 
         // Assert
         verify(mPowerHalService).onPropertySetError(propId, areaId, errorCode);
@@ -215,7 +225,8 @@
         int areaId = VehicleHal.NO_AREA;
 
         // Act
-        mVehicleHal.onPropertySetError(errorCode, propId, areaId);
+        mHandler.post(() -> mVehicleHal.onPropertySetError(errorCode, propId, areaId));
+        CarServiceUtils.runOnLooperSync(mHandlerThread.getLooper(), () -> {});
 
         // Assert
         verify(mPowerHalService).onPropertySetError(propId, areaId, errorCode);
diff --git a/tests/carservice_unit_test/src/com/android/car/pm/CarSafetyAccessibilityServiceTest.java b/tests/carservice_unit_test/src/com/android/car/pm/CarSafetyAccessibilityServiceTest.java
new file mode 100644
index 0000000..7494f5d
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/pm/CarSafetyAccessibilityServiceTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.pm;
+
+
+import static org.mockito.Mockito.verify;
+
+import android.car.AbstractExtendedMockitoCarServiceTestCase;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.car.CarLocalServices;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+@RunWith(JUnit4.class)
+public class CarSafetyAccessibilityServiceTest extends AbstractExtendedMockitoCarServiceTestCase {
+    @Mock
+    private CarPackageManagerService mMockCarPackageManagerService;
+
+    private CarSafetyAccessibilityService mCarSafetyAccessibilityService;
+
+    @Before
+    public void setup() {
+        mockGetCarLocalService(CarPackageManagerService.class, mMockCarPackageManagerService);
+        mCarSafetyAccessibilityService = new CarSafetyAccessibilityService();
+    }
+
+    @Test
+    public void onAccessibilityEvent_carPackageManagerServiceNotified() {
+        mCarSafetyAccessibilityService.onAccessibilityEvent(new AccessibilityEvent());
+
+        verify(mMockCarPackageManagerService).onWindowChangeEvent();
+    }
+
+    @Override
+    protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
+        session.spyStatic(CarLocalServices.class);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/pm/WindowDumpParserTest.java b/tests/carservice_unit_test/src/com/android/car/pm/WindowDumpParserTest.java
new file mode 100644
index 0000000..b64c6e5
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/pm/WindowDumpParserTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class WindowDumpParserTest {
+    private static final String WINDOW_DUMP = "WINDOW MANAGER WINDOWS (dumpsys window windows)\n"
+            + "  Window #0 Window{ccae0fb u10 com.app1}:\n"
+            + "    mDisplayId=0 rootTaskId=1000006 mSession=Session{ef1bfd2 2683:u10a10088} "
+            + "mClient=android.os.BinderProxy@46bd28a\n"
+            + "    mOwnerUid=1010088 showForAllUsers=false package=com.app1 "
+            + "appop=SYSTEM_ALERT_WINDOW\n"
+            + "    mAttrs={(0,0)(0x0) gr=TOP RIGHT CENTER sim={adjust=pan} ty=APPLICATION_OVERLAY"
+            + " fmt=TRANSPARENT\n"
+            + "    isOnScreen=true\n"
+            + "    isVisible=true\n"
+
+            + "  Window #1 Window{17aaef4 u0 App 2}:\n"
+            + "    mDisplayId=1 rootTaskId=1000006 mSession=Session{629ba4e 2235:u0a10120} "
+            + "mClient=android.os.Binderproxy@3f3ea06\n"
+            + "    mOwnerUid=10120 showForAllUsers=true package=com.app2 appop=NONE\n"
+            + "    mAttrs={(24,0)(84x419) gr=BOTTOM RIGHT CENTER sim={adjust=pan} "
+            + "ty=DISPLAY_OVERLAY fmt=TRANSLUCENT\n"
+            + "      fl=NOT_FOCUSABLE LAYOUT_NO_LIMITS HARDWARE_ACCELERATED\n"
+            + "      pfl=NO_MOVE_ANIMATION USE_BLAST INSET_PARENT_FRAME_BY_IME\n"
+            + "      bhv=DEFAULT\n"
+            + "      fitTypes=STATUS_BARS NAVIGATION_BARS CAPTION_BAR}\n"
+            + "    Requested w=1080 h=1920 mLayoutSeq=154\n"
+            + "    mBaseLayer=21000 mSubLayer=0    mToken=AppWindowToken{546fe66 token=Token"
+            + "{8b7a6c1 ActivityRecord{2789ba8 u0 com.app2/.MainActivity t5}}}\n"
+            + "    mAppToken=AppWindowToken{546fe66 token=Token{8b7a6c1 ActivityRecord{2789ba8 u0"
+            + " com.app2/.MainActivity t5}}}\n"
+
+            + ".BinderProxy@3f3ea06}\n"
+            + "    Frames: containing=[0,0][1080,600] parent=[0,0][1080,600] display=[-10000,"
+            + "-10000][10000,10000]\n"
+            + "    mFrame=[972,181][1056,600] last=[0,0][0,0]\n"
+            + "     surface=[0,0][0,0]\n"
+            + "    WindowStateAnimator{8edd4a7 HVAC Passenger Temp}:\n"
+            + "      mDrawState=NO_SURFACE       mLastHidden=false\n"
+            + "      mEnterAnimationPending=false      mSystemDecorRect=[0,0][0,0]\n"
+            + "      mShownAlpha=0.0 mAlpha=1.0 mLastAlpha=0.0\n"
+            + "    mForceSeamlesslyRotate=false seamlesslyRotate: pending=null "
+            + "finishedFrameNumber=0\n"
+            + "    isOnScreen=false\n"
+            + "    isVisible=false\n"
+
+            + "  Window #2 Window{1c5571 u0 HVAC Driver Temp}:\n"
+            + "    mDisplayId=1 rootTaskId=1000006 mSession=Session{629ba4e 2235:u0a10120} "
+            + "mClient=android.os.BinderProxy@99ccafb\n"
+            + "    mOwnerUid=10120 showForAllUsers=true package=com.app2 appop=NONE\n"
+            + "    mAttrs={(24,0)(84x419) gr=BOTTOM LEFT CENTER sim={adjust=pan} "
+            + "ty=DISPLAY_OVERLAY fmt=TRANSLUCENT\n"
+            + "      fl=NOT_FOCUSABLE LAYOUT_NO_LIMITS HARDWARE_ACCELERATED\n"
+            + "      pfl=NO_MOVE_ANIMATION USE_BLAST INSET_PARENT_FRAME_BY_IME\n"
+            + "      bhv=DEFAULT\n"
+            + "      fitTypes=STATUS_BARS NAVIGATION_BARS CAPTION_BAR}\n"
+            + "    Requested w=84 h=419 mLayoutSeq=143\n"
+            + "    mBaseLayer=21000 mSubLayer=0    mToken=ActivityRecord{b44066 u10 com.app2/"
+            + "SecondActivity t1000031}\n"
+            + "    mActivityRecord=ActivityRecord{b44066 u10 com.app2/SecondActivity t1000031}\n"
+            + ".BinderProxy@99ccafb}\n"
+            + "    mViewVisibility=0x4 mHaveFrame=true mObscured=false\n"
+            + "    mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]\n"
+            + "    isOnScreen=false\n"
+            + "    isVisible=false\n"
+
+            + "  Window #3 Window{1c5571 u0 HVAC Driver Temp}:\n"
+            + "    mDisplayId=2 rootTaskId=1000006 mSession=Session{629ba4e 2235:u0a10120} "
+            + "mClient=android.os.BinderProxy@99ccafb\n"
+            + "    mOwnerUid=10120 showForAllUsers=true package=com.app3 appop=NONE\n"
+            + "    mAttrs={(24,0)(84x419) gr=BOTTOM LEFT CENTER sim={adjust=pan} "
+            + "ty=DISPLAY_OVERLAY fmt=TRANSLUCENT\n"
+            + "      fl=NOT_FOCUSABLE LAYOUT_NO_LIMITS HARDWARE_ACCELERATED\n"
+            + "      pfl=NO_MOVE_ANIMATION USE_BLAST INSET_PARENT_FRAME_BY_IME\n"
+            + "      bhv=DEFAULT\n"
+            + "      fitTypes=STATUS_BARS NAVIGATION_BARS CAPTION_BAR}\n"
+            + "    Requested w=84 h=419 mLayoutSeq=143\n"
+            + "    mBaseLayer=291000 mSubLayer=0    mToken=WindowToken{6bd1718 android.os"
+            + "    mActivityRecord=ActivityRecord{a3f066 u10 com.app3/MainActivity t1000031}\n"
+            + ".BinderProxy@99ccafb}\n"
+            + "    mViewVisibility=0x4 mHaveFrame=true mObscured=false\n"
+            + "    mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]\n"
+            + "    isOnScreen=false\n"
+            + "    isVisible=false\n"
+
+            + "  Window #4 Window{1c5571 u0 HVAC Driver Temp}:\n"
+            + "    mDisplayId=2 rootTaskId=1000006 mSession=Session{629ba4e 2235:u0a10120} "
+            + "mClient=android.os.BinderProxy@99ccafb\n"
+            + "    mOwnerUid=10120 showForAllUsers=true package=com.app3 appop=NONE\n"
+            + "    mAttrs={(24,0)(84x419) gr=BOTTOM LEFT CENTER sim={adjust=pan} "
+            + "ty=APPLICATION_STARTING fmt=TRANSLUCENT\n"
+            + "      fl=NOT_FOCUSABLE LAYOUT_NO_LIMITS HARDWARE_ACCELERATED\n"
+            + "      pfl=NO_MOVE_ANIMATION USE_BLAST INSET_PARENT_FRAME_BY_IME\n"
+            + "      bhv=DEFAULT\n"
+            + "      fitTypes=STATUS_BARS NAVIGATION_BARS CAPTION_BAR}\n"
+            + "    Requested w=84 h=419 mLayoutSeq=143\n"
+            + "    mBaseLayer=291000 mSubLayer=0    mToken=WindowToken{6bd1718 android.os"
+            + ".BinderProxy@99ccafb}\n"
+            + "    mViewVisibility=0x4 mHaveFrame=true mObscured=false\n"
+            + "    mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]\n"
+            + "    isOnScreen=false\n"
+            + "    isVisible=false\n";
+
+    private static final WindowDumpParser.Window APP_1_WINDOW = new WindowDumpParser.Window(
+            "com.app1", 0, null);
+    private static final WindowDumpParser.Window APP_2_WINDOW = new WindowDumpParser.Window(
+            "com.app2", 1, "2789ba8 u0 com.app2/.MainActivity t5");
+    private static final WindowDumpParser.Window APP_2_WINDOW_2 = new WindowDumpParser.Window(
+            "com.app2", 1, "b44066 u10 com.app2/SecondActivity t1000031");
+    private static final WindowDumpParser.Window APP_3_WINDOW = new WindowDumpParser.Window(
+            "com.app3", 2, "a3f066 u10 com.app3/MainActivity t1000031");
+
+    @Test
+    public void testWindowDumpParsing() {
+        assertThat(WindowDumpParser.getParsedAppWindows(WINDOW_DUMP, "com.app1")).containsExactly(
+                APP_1_WINDOW);
+        assertThat(WindowDumpParser.getParsedAppWindows(WINDOW_DUMP, "com.app2")).containsExactly(
+                APP_2_WINDOW, APP_2_WINDOW_2);
+        assertThat(WindowDumpParser.getParsedAppWindows(WINDOW_DUMP, "com.app3")).containsExactly(
+                APP_3_WINDOW);
+    }
+}
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..6b4254a 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
@@ -17,107 +17,262 @@
 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.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+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 android.os.PersistableBundle;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.car.CarLocalServices;
+import com.android.car.CarPropertyService;
+import com.android.car.systeminterface.SystemInterface;
+import com.android.car.systeminterface.SystemStateInterface;
+
 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.ByteArrayOutputStream;
+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;
+    private MetricsConfigStore mMetricsConfigStore;
+    private ResultStore mResultStore;
+
     @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;
+    @Mock
+    private SystemStateInterface mMockSystemStateInterface;
 
     @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);
+        when(mMockSystemInterface.getSystemStateInterface()).thenReturn(mMockSystemStateInterface);
+
+        mService = new CarTelemetryService(mContext, mMockCarPropertyService);
+        mService.init();
+        mService.setListener(mMockListener);
+
+        mTelemetryHandler = mService.getTelemetryHandler();
+        mTelemetryHandler.getLooper().getQueue().addIdleHandler(() -> {
+            mIdleHandlerLatch.countDown();
+            return true;
+        });
+        waitForHandlerThreadToFinish();
+
+        mMetricsConfigStore = mService.getMetricsConfigStore();
+        mResultStore = mService.getResultStore();
     }
 
     @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 config".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_shouldReplaceAndDeleteOldResult()
+            throws Exception {
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+        mResultStore.putInterimResult(KEY_V1.getName(), new PersistableBundle());
 
-        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));
+        assertThat(mMetricsConfigStore.getActiveMetricsConfigs())
+                .containsExactly(METRICS_CONFIG_V2);
+        assertThat(mResultStore.getInterimResult(KEY_V1.getName())).isNull();
     }
 
     @Test
-    public void testRemoveManifest_manifestExists_shouldSucceed() {
-        mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+    public void testRemoveMetricsConfig_configExists_shouldDeleteScriptResult() throws Exception {
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+        mResultStore.putInterimResult(KEY_V1.getName(), new PersistableBundle());
 
-        boolean result = mService.removeManifest(mManifestKeyV1);
+        mService.removeMetricsConfig(KEY_V1);
 
-        assertThat(result).isTrue();
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onRemoveMetricsConfigStatus(eq(KEY_V1), eq(true));
+        assertThat(mMetricsConfigStore.getActiveMetricsConfigs()).isEmpty();
+        assertThat(mResultStore.getInterimResult(KEY_V1.getName())).isNull();
     }
 
     @Test
-    public void testRemoveManifest_manifestDoesNotExist_shouldFail() {
-        boolean result = mService.removeManifest(mManifestKeyV1);
+    public void testRemoveMetricsConfig_configDoesNotExist_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());
+    public void testRemoveAllMetricsConfigs_shouldRemoveConfigsAndResults() throws Exception {
+        MetricsConfigKey key = new MetricsConfigKey("test config", 2);
+        TelemetryProto.MetricsConfig config =
+                TelemetryProto.MetricsConfig.newBuilder().setName(key.getName()).build();
+        mService.addMetricsConfig(key, config.toByteArray());
+        mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+        mResultStore.putInterimResult(KEY_V1.getName(), new PersistableBundle());
+        mResultStore.putFinalResult(key.getName(), new PersistableBundle());
 
-        mService.removeAllManifests();
+        mService.removeAllMetricsConfigs();
 
-        // 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);
+        waitForHandlerThreadToFinish();
+        assertThat(mMetricsConfigStore.getActiveMetricsConfigs()).isEmpty();
+        assertThat(mResultStore.getInterimResult(KEY_V1.getName())).isNull();
+        assertThat(mResultStore.getFinalResult(key.getName(), /* deleteResult = */ false)).isNull();
+    }
+
+    @Test
+    public void testSendFinishedReports_whenNoReport_shouldNotReceiveResponse() throws Exception {
+        mService.sendFinishedReports(KEY_V1);
+
+        waitForHandlerThreadToFinish();
+        verify(mMockListener, never()).onResult(any(), any());
+        verify(mMockListener, never()).onError(any(), any());
+    }
+
+    @Test
+    public void testSendFinishedReports_whenFinalResult_shouldReceiveResult() throws Exception {
+        PersistableBundle finalResult = new PersistableBundle();
+        finalResult.putBoolean("finished", true);
+        mResultStore.putFinalResult(KEY_V1.getName(), finalResult);
+
+        mService.sendFinishedReports(KEY_V1);
+
+        waitForHandlerThreadToFinish();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        finalResult.writeToStream(bos);
+        verify(mMockListener).onResult(eq(KEY_V1), eq(bos.toByteArray()));
+        // result should have been deleted
+        assertThat(mResultStore.getFinalResult(KEY_V1.getName(), false)).isNull();
+    }
+
+    @Test
+    public void testSendFinishedReports_whenError_shouldReceiveError() throws Exception {
+        TelemetryProto.TelemetryError error = TelemetryProto.TelemetryError.newBuilder()
+                .setErrorType(TelemetryProto.TelemetryError.ErrorType.LUA_RUNTIME_ERROR)
+                .setMessage("test error")
+                .build();
+        mResultStore.putError(KEY_V1.getName(), error);
+
+        mService.sendFinishedReports(KEY_V1);
+
+        waitForHandlerThreadToFinish();
+        verify(mMockListener).onError(eq(KEY_V1), eq(error.toByteArray()));
+        // error should have been deleted
+        assertThat(mResultStore.getError(KEY_V1.getName(), false)).isNull();
+    }
+
+    @Test
+    public void testSendFinishedReports_whenListenerNotSet_shouldDoNothing() throws Exception {
+        PersistableBundle finalResult = new PersistableBundle();
+        finalResult.putBoolean("finished", true);
+        mResultStore.putFinalResult(KEY_V1.getName(), finalResult);
+        mService.clearListener(); // no listener = no way to send back results
+
+        mService.sendFinishedReports(KEY_V1);
+
+        waitForHandlerThreadToFinish();
+        // if listener is null, nothing should be done, result should still be in result store
+        assertThat(mResultStore.getFinalResult(KEY_V1.getName(), false).toString())
+                .isEqualTo(finalResult.toString());
+    }
+
+    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/JniUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/JniUtilsTest.java
deleted file mode 100644
index 67a5ac8..0000000
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/JniUtilsTest.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.telemetry;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Bundle;
-import android.util.Log;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public final class JniUtilsTest {
-
-    private static final String TAG = JniUtilsTest.class.getSimpleName();
-
-    private static final String BOOLEAN_KEY = "boolean_key";
-    private static final String INT_KEY = "int_key";
-    private static final String STRING_KEY = "string_key";
-    private static final String NUMBER_KEY = "number_key";
-
-    private static final boolean BOOLEAN_VALUE = true;
-    private static final double NUMBER_VALUE = 0.1;
-    private static final int INT_VALUE = 10;
-    private static final String STRING_VALUE = "test";
-
-    // Pointer to Lua Engine instantiated in native space.
-    private long mLuaEnginePtr = 0;
-
-    static {
-        System.loadLibrary("scriptexecutorjniutils-test");
-    }
-
-    @Before
-    public void setUp() {
-        mLuaEnginePtr = nativeCreateLuaEngine();
-    }
-
-    @After
-    public void tearDown() {
-        nativeDestroyLuaEngine(mLuaEnginePtr);
-    }
-
-    // Simply invokes PushBundleToLuaTable native method under test.
-    private native void nativePushBundleToLuaTableCaller(long luaEnginePtr, Bundle bundle);
-
-    // Creates an instance of LuaEngine on the heap and returns the pointer.
-    private native long nativeCreateLuaEngine();
-
-    // Destroys instance of LuaEngine on the native side at provided memory address.
-    private native void nativeDestroyLuaEngine(long luaEnginePtr);
-
-    // Returns size of a Lua object located at the specified position on the stack.
-    private native int nativeGetObjectSize(long luaEnginePtr, int index);
-
-    /*
-     * Family of methods to check if the table on top of the stack has
-     * the given value under provided key.
-     */
-    private native boolean nativeHasBooleanValue(long luaEnginePtr, String key, boolean value);
-    private native boolean nativeHasStringValue(long luaEnginePtr, String key, String value);
-    private native boolean nativeHasIntValue(long luaEnginePtr, String key, int value);
-    private native boolean nativeHasDoubleValue(long luaEnginePtr, String key, double value);
-
-    @Test
-    public void pushBundleToLuaTable_nullBundleMakesEmptyLuaTable() {
-        Log.d(TAG, "Using Lua Engine with address " + mLuaEnginePtr);
-        nativePushBundleToLuaTableCaller(mLuaEnginePtr, null);
-        // Get the size of the object on top of the stack,
-        // which is where our table is supposed to be.
-        assertThat(nativeGetObjectSize(mLuaEnginePtr, 1)).isEqualTo(0);
-    }
-
-    @Test
-    public void pushBundleToLuaTable_valuesOfDifferentTypes() {
-        Bundle bundle = new Bundle();
-        bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
-        bundle.putInt(INT_KEY, INT_VALUE);
-        bundle.putDouble(NUMBER_KEY, NUMBER_VALUE);
-        bundle.putString(STRING_KEY, STRING_VALUE);
-
-        // Invokes the corresponding helper method to convert the bundle
-        // to Lua table on Lua stack.
-        nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
-
-        // Check contents of Lua table.
-        assertThat(nativeHasBooleanValue(mLuaEnginePtr, BOOLEAN_KEY, BOOLEAN_VALUE)).isTrue();
-        assertThat(nativeHasIntValue(mLuaEnginePtr, INT_KEY, INT_VALUE)).isTrue();
-        assertThat(nativeHasDoubleValue(mLuaEnginePtr, NUMBER_KEY, NUMBER_VALUE)).isTrue();
-        assertThat(nativeHasStringValue(mLuaEnginePtr, STRING_KEY, STRING_VALUE)).isTrue();
-    }
-
-
-    @Test
-    public void pushBundleToLuaTable_wrongKey() {
-        Bundle bundle = new Bundle();
-        bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
-
-        // Invokes the corresponding helper method to convert the bundle
-        // to Lua table on Lua stack.
-        nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
-
-        // Check contents of Lua table.
-        assertThat(nativeHasBooleanValue(mLuaEnginePtr, "wrong key", BOOLEAN_VALUE)).isFalse();
-    }
-}
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..daf83d8
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/MetricsConfigStoreTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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();
+    }
+
+    @Test
+    public void testDeleteAllMetricsConfigs_shouldDeleteAll() throws Exception {
+        writeConfigToDisk(METRICS_CONFIG_FOO);
+        writeConfigToDisk(METRICS_CONFIG_BAR);
+
+        mMetricsConfigStore.deleteAllMetricsConfigs();
+
+        assertThat(mTestMetricsConfigDir.listFiles()).isEmpty();
+    }
+
+    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
new file mode 100644
index 0000000..6eb8fd6
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/ResultStoreTest.java
@@ -0,0 +1,295 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertWithMessage;
+
+import android.os.PersistableBundle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ResultStoreTest {
+    private static final PersistableBundle TEST_INTERIM_BUNDLE = new PersistableBundle();
+    private static final PersistableBundle TEST_FINAL_BUNDLE = new PersistableBundle();
+    private static final TelemetryProto.TelemetryError TEST_TELEMETRY_ERROR =
+            TelemetryProto.TelemetryError.newBuilder().setMessage("test error").build();
+
+    private File mTestRootDir;
+    private File mTestInterimResultDir;
+    private File mTestErrorResultDir;
+    private File mTestFinalResultDir;
+    private ResultStore mResultStore;
+
+    @Before
+    public void setUp() throws Exception {
+        TEST_INTERIM_BUNDLE.putString("test key", "interim value");
+        TEST_FINAL_BUNDLE.putString("test key", "final value");
+
+        mTestRootDir = Files.createTempDirectory("car_telemetry_test").toFile();
+        mTestInterimResultDir = new File(mTestRootDir, ResultStore.INTERIM_RESULT_DIR);
+        mTestErrorResultDir = new File(mTestRootDir, ResultStore.ERROR_RESULT_DIR);
+        mTestFinalResultDir = new File(mTestRootDir, ResultStore.FINAL_RESULT_DIR);
+
+        mResultStore = new ResultStore(mTestRootDir);
+    }
+
+    @Test
+    public void testConstructor_shouldCreateResultsFolder() {
+        // constructor is called in setUp()
+        assertThat(mTestInterimResultDir.exists()).isTrue();
+        assertThat(mTestFinalResultDir.exists()).isTrue();
+    }
+
+    @Test
+    public void testConstructor_shouldLoadInterimResultsIntoMemory() throws Exception {
+        String testInterimFileName = "test_file_1";
+        writeBundleToFile(mTestInterimResultDir, testInterimFileName, TEST_INTERIM_BUNDLE);
+
+        mResultStore = new ResultStore(mTestRootDir);
+
+        // should compare value instead of reference
+        assertThat(mResultStore.getInterimResult(testInterimFileName).toString())
+                .isEqualTo(TEST_INTERIM_BUNDLE.toString());
+    }
+
+    @Test
+    public void testFlushToDisk_shouldWriteResultsToFileAndCheckContent() throws Exception {
+        String testInterimFileName = "test_file_1";
+        String testFinalFileName = "test_file_2";
+        writeBundleToFile(mTestInterimResultDir, testInterimFileName, TEST_INTERIM_BUNDLE);
+        writeBundleToFile(mTestFinalResultDir, testFinalFileName, TEST_FINAL_BUNDLE);
+
+        mResultStore.flushToDisk();
+
+        assertThat(new File(mTestInterimResultDir, testInterimFileName).exists()).isTrue();
+        assertThat(new File(mTestFinalResultDir, testFinalFileName).exists()).isTrue();
+        // the content check will need to be modified when data encryption is implemented
+        PersistableBundle interimData =
+                readBundleFromFile(mTestInterimResultDir, testInterimFileName);
+        assertThat(interimData.toString()).isEqualTo(TEST_INTERIM_BUNDLE.toString());
+        PersistableBundle finalData = readBundleFromFile(mTestFinalResultDir, testFinalFileName);
+        assertThat(finalData.toString()).isEqualTo(TEST_FINAL_BUNDLE.toString());
+    }
+
+    @Test
+    public void testFlushToDisk_shouldRemoveStaleData() throws Exception {
+        File staleTestFile1 = new File(mTestInterimResultDir, "stale_test_file_1");
+        File staleTestFile2 = new File(mTestFinalResultDir, "stale_test_file_2");
+        File activeTestFile3 = new File(mTestInterimResultDir, "active_test_file_3");
+        writeBundleToFile(staleTestFile1, TEST_INTERIM_BUNDLE);
+        writeBundleToFile(staleTestFile2, TEST_FINAL_BUNDLE);
+        writeBundleToFile(activeTestFile3, TEST_INTERIM_BUNDLE);
+        long currTimeMs = System.currentTimeMillis();
+        staleTestFile1.setLastModified(0L); // stale
+        staleTestFile2.setLastModified(
+                currTimeMs - TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS)); // stale
+        activeTestFile3.setLastModified(
+                currTimeMs - TimeUnit.MILLISECONDS.convert(29, TimeUnit.DAYS)); // active
+
+        mResultStore.flushToDisk();
+
+        assertThat(staleTestFile1.exists()).isFalse();
+        assertThat(staleTestFile2.exists()).isFalse();
+        assertThat(activeTestFile3.exists()).isTrue();
+    }
+
+    @Test
+    public void testGetFinalResult_whenNoData_shouldReceiveNull() throws Exception {
+        String metricsConfigName = "my_metrics_config";
+
+        PersistableBundle bundle = mResultStore.getFinalResult(metricsConfigName, true);
+
+        assertThat(bundle).isNull();
+    }
+
+    @Test
+    public void testGetFinalResult_whenDataCorrupt_shouldReceiveNull() throws Exception {
+        String metricsConfigName = "my_metrics_config";
+        Files.write(new File(mTestFinalResultDir, metricsConfigName).toPath(),
+                "not a bundle".getBytes(StandardCharsets.UTF_8));
+
+        PersistableBundle bundle = mResultStore.getFinalResult(metricsConfigName, true);
+
+        assertThat(bundle).isNull();
+    }
+
+    @Test
+    public void testGetFinalResult_whenDeleteFlagTrue_shouldDeleteData() throws Exception {
+        String testFinalFileName = "my_metrics_config";
+        writeBundleToFile(mTestFinalResultDir, testFinalFileName, TEST_FINAL_BUNDLE);
+
+        PersistableBundle bundle = mResultStore.getFinalResult(testFinalFileName, true);
+
+        // should compare value instead of reference
+        assertThat(bundle.toString()).isEqualTo(TEST_FINAL_BUNDLE.toString());
+        assertThat(new File(mTestFinalResultDir, testFinalFileName).exists()).isFalse();
+    }
+
+    @Test
+    public void testGetFinalResult_whenDeleteFlagFalse_shouldNotDeleteData() throws Exception {
+        String testFinalFileName = "my_metrics_config";
+        writeBundleToFile(mTestFinalResultDir, testFinalFileName, TEST_FINAL_BUNDLE);
+
+        PersistableBundle bundle = mResultStore.getFinalResult(testFinalFileName, false);
+
+        // should compare value instead of reference
+        assertThat(bundle.toString()).isEqualTo(TEST_FINAL_BUNDLE.toString());
+        assertThat(new File(mTestFinalResultDir, testFinalFileName).exists()).isTrue();
+    }
+
+    @Test
+    public void testGetError_whenNoError_shouldReceiveNull() {
+        String metricsConfigName = "my_metrics_config";
+
+        TelemetryProto.TelemetryError error = mResultStore.getError(metricsConfigName, true);
+
+        assertThat(error).isNull();
+    }
+
+    @Test
+    public void testGetError_shouldReceiveError() throws Exception {
+        String metricsConfigName = "my_metrics_config";
+        // write serialized error object to file
+        Files.write(
+                new File(mTestErrorResultDir, metricsConfigName).toPath(),
+                TEST_TELEMETRY_ERROR.toByteArray());
+
+        TelemetryProto.TelemetryError error = mResultStore.getError(metricsConfigName, true);
+
+        assertThat(error).isEqualTo(TEST_TELEMETRY_ERROR);
+    }
+
+    @Test
+    public void testPutInterimResultAndFlushToDisk_shouldReplaceExistingFile() throws Exception {
+        String newKey = "new key";
+        String newValue = "new value";
+        String metricsConfigName = "my_metrics_config";
+        writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
+        TEST_INTERIM_BUNDLE.putString(newKey, newValue);
+
+        mResultStore.putInterimResult(metricsConfigName, TEST_INTERIM_BUNDLE);
+        mResultStore.flushToDisk();
+
+        PersistableBundle bundle = readBundleFromFile(mTestInterimResultDir, metricsConfigName);
+        assertThat(bundle.getString(newKey)).isEqualTo(newValue);
+        assertThat(bundle.toString()).isEqualTo(TEST_INTERIM_BUNDLE.toString());
+    }
+
+    @Test
+    public void testPutInterimResultAndFlushToDisk_shouldWriteDirtyResultsOnly() throws Exception {
+        File fileFoo = new File(mTestInterimResultDir, "foo");
+        File fileBar = new File(mTestInterimResultDir, "bar");
+        writeBundleToFile(fileFoo, TEST_INTERIM_BUNDLE);
+        writeBundleToFile(fileBar, TEST_INTERIM_BUNDLE);
+        mResultStore = new ResultStore(mTestRootDir); // re-load data
+        PersistableBundle newData = new PersistableBundle();
+        newData.putDouble("pi", 3.1415926);
+
+        mResultStore.putInterimResult("bar", newData); // make bar dirty
+        fileFoo.delete(); // delete the clean file from the file system
+        mResultStore.flushToDisk(); // write dirty data
+
+        // foo is a clean file that should not be written in shutdown
+        assertThat(fileFoo.exists()).isFalse();
+        assertThat(readBundleFromFile(fileBar).toString()).isEqualTo(newData.toString());
+    }
+
+    @Test
+    public void testPutFinalResult_shouldWriteResultAndRemoveInterimq() throws Exception {
+        String metricsConfigName = "my_metrics_config";
+        writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
+
+        mResultStore.putFinalResult(metricsConfigName, TEST_FINAL_BUNDLE);
+
+        assertThat(mResultStore.getInterimResult(metricsConfigName)).isNull();
+        assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
+        assertThat(new File(mTestFinalResultDir, metricsConfigName).exists()).isTrue();
+    }
+
+    @Test
+    public void testPutError_shouldWriteErrorAndRemoveInterimResultFile() throws Exception {
+        String metricsConfigName = "my_metrics_config";
+        writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
+
+        mResultStore.putError(metricsConfigName, TEST_TELEMETRY_ERROR);
+
+        assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
+        assertThat(new File(mTestErrorResultDir, metricsConfigName).exists()).isTrue();
+    }
+
+    @Test
+    public void testDeleteResult_whenInterimResult_shouldDelete() throws Exception {
+        String metricsConfigName = "my_metrics_config";
+        writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
+
+        mResultStore.deleteResult(metricsConfigName);
+
+        assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
+    }
+
+    @Test
+    public void testDeleteResult_whenFinalResult_shouldDelete() throws Exception {
+        String metricsConfigName = "my_metrics_config";
+        writeBundleToFile(mTestFinalResultDir, metricsConfigName, TEST_FINAL_BUNDLE);
+
+        mResultStore.deleteResult(metricsConfigName);
+
+        assertThat(new File(mTestFinalResultDir, metricsConfigName).exists()).isFalse();
+    }
+
+    private void writeBundleToFile(
+            File dir, String fileName, PersistableBundle persistableBundle) throws Exception {
+        writeBundleToFile(new File(dir, fileName), persistableBundle);
+    }
+
+    /**
+     * Writes a persistable bundle to the result directory with the given directory and file name,
+     * and verifies that it was successfully written.
+     */
+    private void writeBundleToFile(
+            File file, PersistableBundle persistableBundle) throws Exception {
+        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
+            persistableBundle.writeToStream(byteArrayOutputStream);
+            Files.write(file.toPath(), byteArrayOutputStream.toByteArray());
+        }
+        assertWithMessage("bundle is not written to the result directory")
+                .that(file.exists()).isTrue();
+    }
+
+    private PersistableBundle readBundleFromFile(File dir, String fileName) throws Exception {
+        return readBundleFromFile(new File(dir, fileName));
+    }
+
+    /** Reads a persistable bundle from the given path. */
+    private PersistableBundle readBundleFromFile(File file) throws Exception {
+        try (FileInputStream fis = new FileInputStream(file)) {
+            return PersistableBundle.readFromStream(fis);
+        }
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
deleted file mode 100644
index 774da25..0000000
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.telemetry;
-
-import static org.junit.Assert.fail;
-
-import android.car.telemetry.IScriptExecutor;
-import android.car.telemetry.IScriptExecutorListener;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(JUnit4.class)
-public final class ScriptExecutorTest {
-
-    private IScriptExecutor mScriptExecutor;
-    private ScriptExecutor mInstance;
-    private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
-
-    private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
-        @Override
-        public void onScriptFinished(byte[] result) {
-        }
-
-        @Override
-        public void onSuccess(Bundle stateToPersist) {
-        }
-
-        @Override
-        public void onError(int errorType, String message, String stackTrace) {
-        }
-    }
-
-    private final IScriptExecutorListener mFakeScriptExecutorListener =
-            new ScriptExecutorListener();
-
-    // 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 static final String LUA_SCRIPT =
-            "function hello(data, state)\n"
-            + "    print(\"Hello World\")\n"
-            + "end\n";
-
-    private static final String LUA_METHOD = "hello";
-
-    private final CountDownLatch mBindLatch = new CountDownLatch(1);
-
-    private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
-
-    private final ServiceConnection mScriptExecutorConnection =
-            new ServiceConnection() {
-                @Override
-                public void onServiceConnected(ComponentName className, IBinder service) {
-                    mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
-                    mBindLatch.countDown();
-                }
-
-                @Override
-                public void onServiceDisconnected(ComponentName className) {
-                    fail("Service unexpectedly disconnected");
-                }
-            };
-
-    @Before
-    public void setUp() throws InterruptedException {
-        mContext.bindIsolatedService(new Intent(mContext, ScriptExecutor.class),
-                Context.BIND_AUTO_CREATE, "scriptexecutor", mContext.getMainExecutor(),
-                mScriptExecutorConnection);
-        if (!mBindLatch.await(BIND_SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
-            fail("Failed to bind to ScripExecutor service");
-        }
-    }
-
-    @Test
-    public void invokeScript_helloWorld() throws RemoteException {
-        // Expect to load "hello world" script successfully and push the function.
-        mScriptExecutor.invokeScript(LUA_SCRIPT, LUA_METHOD, mPublishedData, mSavedState,
-                mFakeScriptExecutorListener);
-        // Sleep, otherwise the test case will complete before the script loads
-        // because invokeScript is non-blocking.
-        try {
-            // TODO(b/192285332): Replace sleep logic with waiting for specific callbacks
-            // to be called once they are implemented. Otherwise, this could be a flaky test.
-            TimeUnit.SECONDS.sleep(10);
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-            fail(e.getMessage());
-        }
-    }
-}
-
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerTest.java
new file mode 100644
index 0000000..43fe336
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.databroker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Handler;
+
+import com.android.car.systeminterface.SystemStateInterface;
+import com.android.car.telemetry.MetricsConfigStore;
+import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.systemmonitor.SystemMonitor;
+import com.android.car.telemetry.systemmonitor.SystemMonitorEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DataBrokerControllerTest {
+
+    @Mock private DataBroker mMockDataBroker;
+    @Mock private Handler mMockHandler;
+    @Mock private MetricsConfigStore mMockMetricsConfigStore;
+    @Mock private SystemMonitor mMockSystemMonitor;
+    @Mock private SystemStateInterface mMockSystemStateInterface;
+
+    @Captor ArgumentCaptor<TelemetryProto.MetricsConfig> mConfigCaptor;
+
+    @Captor ArgumentCaptor<Integer> mPriorityCaptor;
+
+    @InjectMocks private DataBrokerController mController;
+
+    private static final TelemetryProto.Publisher PUBLISHER =
+            TelemetryProto.Publisher.newBuilder()
+                                    .setVehicleProperty(
+                                        TelemetryProto.VehiclePropertyPublisher
+                                            .newBuilder()
+                                            .setReadRate(1)
+                                            .setVehiclePropertyId(1000))
+                                    .build();
+    private static final TelemetryProto.Subscriber SUBSCRIBER =
+            TelemetryProto.Subscriber.newBuilder()
+                                     .setHandler("handler_func")
+                                     .setPublisher(PUBLISHER)
+                                     .build();
+    private static final TelemetryProto.MetricsConfig CONFIG =
+            TelemetryProto.MetricsConfig.newBuilder()
+                          .setName("config_name")
+                          .setVersion(1)
+                          .setScript("function init() end")
+                          .addSubscribers(SUBSCRIBER)
+                          .build();
+
+    @Before
+    public void setup() {
+        when(mMockHandler.post(any(Runnable.class))).thenAnswer(i -> {
+            Runnable runnable = i.getArgument(0);
+            runnable.run();
+            return true;
+        });
+    }
+
+    @Test
+    public void testOnNewConfig_configPassedToDataBroker() {
+        mController.onNewMetricsConfig(CONFIG);
+
+        verify(mMockDataBroker).addMetricsConfiguration(mConfigCaptor.capture());
+        assertThat(mConfigCaptor.getValue()).isEqualTo(CONFIG);
+    }
+
+    @Test
+    public void testOnInit_setsOnScriptFinishedCallback() {
+        // Checks that mMockDataBroker's setOnScriptFinishedCallback is called after it's injected
+        // into controller's constructor with @InjectMocks
+        verify(mMockDataBroker).setOnScriptFinishedCallback(
+                any(DataBroker.ScriptFinishedCallback.class));
+    }
+
+    @Test
+    public void testOnBootCompleted_shouldStartMetricsCollection() {
+        when(mMockMetricsConfigStore.getActiveMetricsConfigs()).thenReturn(Arrays.asList(CONFIG));
+        ArgumentCaptor<Runnable> mRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mMockSystemStateInterface).scheduleActionForBootCompleted(
+                mRunnableCaptor.capture(), any());
+
+        mRunnableCaptor.getValue().run(); // startMetricsCollection();
+
+        verify(mMockDataBroker).addMetricsConfiguration(eq(CONFIG));
+    }
+
+    @Test
+    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForHighCpuUsage() {
+        SystemMonitorEvent highCpuEvent = new SystemMonitorEvent();
+        highCpuEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+        highCpuEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+
+        mController.onSystemMonitorEvent(highCpuEvent);
+
+        verify(mMockDataBroker, atLeastOnce())
+            .setTaskExecutionPriority(mPriorityCaptor.capture());
+        assertThat(mPriorityCaptor.getValue())
+            .isEqualTo(DataBrokerController.TASK_PRIORITY_HI);
+    }
+
+    @Test
+    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForHighMemUsage() {
+        SystemMonitorEvent highMemEvent = new SystemMonitorEvent();
+        highMemEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+        highMemEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+
+        mController.onSystemMonitorEvent(highMemEvent);
+
+        verify(mMockDataBroker, atLeastOnce())
+            .setTaskExecutionPriority(mPriorityCaptor.capture());
+        assertThat(mPriorityCaptor.getValue())
+            .isEqualTo(DataBrokerController.TASK_PRIORITY_HI);
+    }
+
+    @Test
+    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForMedCpuUsage() {
+        SystemMonitorEvent medCpuEvent = new SystemMonitorEvent();
+        medCpuEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+        medCpuEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+
+        mController.onSystemMonitorEvent(medCpuEvent);
+
+        verify(mMockDataBroker, atLeastOnce())
+            .setTaskExecutionPriority(mPriorityCaptor.capture());
+        assertThat(mPriorityCaptor.getValue())
+            .isEqualTo(DataBrokerController.TASK_PRIORITY_MED);
+    }
+
+    @Test
+    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForMedMemUsage() {
+        SystemMonitorEvent medMemEvent = new SystemMonitorEvent();
+        medMemEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+        medMemEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+
+        mController.onSystemMonitorEvent(medMemEvent);
+
+        verify(mMockDataBroker, atLeastOnce())
+            .setTaskExecutionPriority(mPriorityCaptor.capture());
+        assertThat(mPriorityCaptor.getValue())
+            .isEqualTo(DataBrokerController.TASK_PRIORITY_MED);
+    }
+
+    @Test
+    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForLowUsage() {
+        SystemMonitorEvent lowUsageEvent = new SystemMonitorEvent();
+        lowUsageEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+        lowUsageEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+
+        mController.onSystemMonitorEvent(lowUsageEvent);
+
+        verify(mMockDataBroker, atLeastOnce())
+            .setTaskExecutionPriority(mPriorityCaptor.capture());
+        assertThat(mPriorityCaptor.getValue())
+            .isEqualTo(DataBrokerController.TASK_PRIORITY_LOW);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerUnitTest.java
deleted file mode 100644
index 931a557..0000000
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerUnitTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.telemetry.databroker;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.verify;
-
-import com.android.car.telemetry.TelemetryProto;
-import com.android.car.telemetry.systemmonitor.SystemMonitor;
-import com.android.car.telemetry.systemmonitor.SystemMonitorEvent;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class DataBrokerControllerUnitTest {
-
-    @Mock private DataBroker mMockDataBroker;
-
-    @Mock private SystemMonitor mMockSystemMonitor;
-
-    @Captor ArgumentCaptor<TelemetryProto.MetricsConfig> mConfigCaptor;
-
-    @Captor ArgumentCaptor<Integer> mPriorityCaptor;
-
-    @InjectMocks private DataBrokerController mController;
-
-    private static final TelemetryProto.Publisher PUBLISHER =
-            TelemetryProto.Publisher.newBuilder()
-                                    .setVehicleProperty(
-                                        TelemetryProto.VehiclePropertyPublisher
-                                            .newBuilder()
-                                            .setReadRate(1)
-                                            .setVehiclePropertyId(1000))
-                                    .build();
-    private static final TelemetryProto.Subscriber SUBSCRIBER =
-            TelemetryProto.Subscriber.newBuilder()
-                                     .setHandler("handler_func")
-                                     .setPublisher(PUBLISHER)
-                                     .build();
-    private static final TelemetryProto.MetricsConfig CONFIG =
-            TelemetryProto.MetricsConfig.newBuilder()
-                          .setName("config_name")
-                          .setVersion(1)
-                          .setScript("function init() end")
-                          .addSubscribers(SUBSCRIBER)
-                          .build();
-
-    @Test
-    public void testOnNewConfig_configPassedToDataBroker() {
-        mController.onNewMetricsConfig(CONFIG);
-
-        verify(mMockDataBroker).addMetricsConfiguration(mConfigCaptor.capture());
-        assertThat(mConfigCaptor.getValue()).isEqualTo(CONFIG);
-    }
-
-    @Test
-    public void testOnInit_setsOnScriptFinishedCallback() {
-        // Checks that mMockDataBroker's setOnScriptFinishedCallback is called after it's injected
-        // into controller's constructor with @InjectMocks
-        verify(mMockDataBroker).setOnScriptFinishedCallback(
-                any(DataBrokerController.ScriptFinishedCallback.class));
-    }
-
-    @Test
-    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForHighCpuUsage() {
-        SystemMonitorEvent highCpuEvent = new SystemMonitorEvent();
-        highCpuEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
-        highCpuEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-
-        mController.onSystemMonitorEvent(highCpuEvent);
-
-        verify(mMockDataBroker, atLeastOnce())
-            .setTaskExecutionPriority(mPriorityCaptor.capture());
-        assertThat(mPriorityCaptor.getValue())
-            .isEqualTo(DataBrokerController.TASK_PRIORITY_HI);
-    }
-
-    @Test
-    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForHighMemUsage() {
-        SystemMonitorEvent highMemEvent = new SystemMonitorEvent();
-        highMemEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-        highMemEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
-
-        mController.onSystemMonitorEvent(highMemEvent);
-
-        verify(mMockDataBroker, atLeastOnce())
-            .setTaskExecutionPriority(mPriorityCaptor.capture());
-        assertThat(mPriorityCaptor.getValue())
-            .isEqualTo(DataBrokerController.TASK_PRIORITY_HI);
-    }
-
-    @Test
-    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForMedCpuUsage() {
-        SystemMonitorEvent medCpuEvent = new SystemMonitorEvent();
-        medCpuEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
-        medCpuEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-
-        mController.onSystemMonitorEvent(medCpuEvent);
-
-        verify(mMockDataBroker, atLeastOnce())
-            .setTaskExecutionPriority(mPriorityCaptor.capture());
-        assertThat(mPriorityCaptor.getValue())
-            .isEqualTo(DataBrokerController.TASK_PRIORITY_MED);
-    }
-
-    @Test
-    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForMedMemUsage() {
-        SystemMonitorEvent medMemEvent = new SystemMonitorEvent();
-        medMemEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-        medMemEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
-
-        mController.onSystemMonitorEvent(medMemEvent);
-
-        verify(mMockDataBroker, atLeastOnce())
-            .setTaskExecutionPriority(mPriorityCaptor.capture());
-        assertThat(mPriorityCaptor.getValue())
-            .isEqualTo(DataBrokerController.TASK_PRIORITY_MED);
-    }
-
-    @Test
-    public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForLowUsage() {
-        SystemMonitorEvent lowUsageEvent = new SystemMonitorEvent();
-        lowUsageEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-        lowUsageEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-
-        mController.onSystemMonitorEvent(lowUsageEvent);
-
-        verify(mMockDataBroker, atLeastOnce())
-            .setTaskExecutionPriority(mPriorityCaptor.capture());
-        assertThat(mPriorityCaptor.getValue())
-            .isEqualTo(DataBrokerController.TASK_PRIORITY_LOW);
-    }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
new file mode 100644
index 0000000..82f7e5e
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
@@ -0,0 +1,507 @@
+/*
+ * 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.databroker;
+
+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.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.car.hardware.CarPropertyConfig;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+
+import com.android.car.CarPropertyService;
+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.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DataBrokerTest {
+    private static final int PROP_ID = 100;
+    private static final int PROP_AREA = 200;
+    private static final int PRIORITY_HIGH = 1;
+    private static final int PRIORITY_LOW = 100;
+    private static final long TIMEOUT_MS = 5_000L;
+    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 TelemetryProto.VehiclePropertyPublisher
+            VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION =
+            TelemetryProto.VehiclePropertyPublisher.newBuilder().setReadRate(
+                    1).setVehiclePropertyId(PROP_ID).build();
+    private static final TelemetryProto.Publisher PUBLISHER_CONFIGURATION =
+            TelemetryProto.Publisher.newBuilder().setVehicleProperty(
+                    VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION).build();
+    private static final TelemetryProto.Subscriber SUBSCRIBER_FOO =
+            TelemetryProto.Subscriber.newBuilder().setHandler("function_name_foo").setPublisher(
+                    PUBLISHER_CONFIGURATION).setPriority(PRIORITY_HIGH).build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_FOO =
+            TelemetryProto.MetricsConfig.newBuilder().setName("Foo").setVersion(
+                    1).addSubscribers(SUBSCRIBER_FOO).build();
+    private static final TelemetryProto.Subscriber SUBSCRIBER_BAR =
+            TelemetryProto.Subscriber.newBuilder().setHandler("function_name_bar").setPublisher(
+                    PUBLISHER_CONFIGURATION).setPriority(PRIORITY_LOW).build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_BAR =
+            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 ScriptExecutionTask mHighPriorityTask;
+    private ScriptExecutionTask mLowPriorityTask;
+
+    @Mock
+    private Context mMockContext;
+    @Mock
+    private CarPropertyService mMockCarPropertyService;
+    @Mock
+    private Handler mMockHandler;
+    @Mock
+    private StatsManagerProxy mMockStatsManager;
+    @Mock
+    private IBinder mMockScriptExecutorBinder;
+    @Mock
+    private ResultStore mMockResultStore;
+
+    @Before
+    public void setUp() throws Exception {
+        when(mMockCarPropertyService.getPropertyList())
+                .thenReturn(Collections.singletonList(PROP_CONFIG));
+        PublisherFactory factory = new PublisherFactory(
+                mMockCarPropertyService, mMockHandler, mMockStatsManager,
+                Files.createTempDirectory("telemetry_test").toFile());
+        mDataBroker = new DataBrokerImpl(mMockContext, factory, mMockResultStore);
+        // 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);
+        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),
+                mData,
+                SystemClock.elapsedRealtime());
+        mLowPriorityTask = new ScriptExecutionTask(
+                new DataSubscriber(mDataBroker, METRICS_CONFIG_BAR, SUBSCRIBER_BAR),
+                mData,
+                SystemClock.elapsedRealtime());
+    }
+
+    @Test
+    public void testSetTaskExecutionPriority_whenNoTask_shouldNotInvokeScriptExecutor()
+            throws Exception {
+        mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
+
+        waitForHandlerThreadToFinish();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testSetTaskExecutionPriority_whenNextTaskPriorityLow_shouldNotRunTask()
+            throws Exception {
+        mDataBroker.getTaskQueue().add(mLowPriorityTask);
+
+        mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
+
+        waitForHandlerThreadToFinish();
+        // task is not polled
+        assertThat(mDataBroker.getTaskQueue().peek()).isEqualTo(mLowPriorityTask);
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testSetTaskExecutionPriority_whenNextTaskPriorityHigh_shouldInvokeScriptExecutor()
+            throws Exception {
+        mDataBroker.getTaskQueue().add(mHighPriorityTask);
+
+        mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
+
+        waitForHandlerThreadToFinish();
+        // task is polled and run
+        assertThat(mDataBroker.getTaskQueue().peek()).isNull();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testScheduleNextTask_whenNoTask_shouldNotInvokeScriptExecutor() throws Exception {
+        mDataBroker.scheduleNextTask();
+
+        waitForHandlerThreadToFinish();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testScheduleNextTask_whenTaskInProgress_shouldNotInvokeScriptExecutorAgain()
+            throws Exception {
+        PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+        taskQueue.add(mHighPriorityTask);
+        mDataBroker.scheduleNextTask(); // start a task
+        waitForHandlerThreadToFinish();
+        assertThat(taskQueue.peek()).isNull(); // assert that task is polled and running
+        taskQueue.add(mHighPriorityTask); // add another task into the queue
+
+        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
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testScheduleNextTask_whenTaskCompletes_shouldAutomaticallyScheduleNextTask()
+            throws Exception {
+        PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+        // add two tasks into the queue for execution
+        taskQueue.add(mHighPriorityTask);
+        taskQueue.add(mHighPriorityTask);
+
+        mDataBroker.scheduleNextTask(); // start a task
+        waitForHandlerThreadToFinish();
+        // end a task, should automatically schedule the next task
+        mFakeScriptExecutor.notifyScriptSuccess(mData); // posts to telemetry handler
+
+        waitForHandlerThreadToFinish();
+        // verify queue is empty, both tasks are polled and executed
+        assertThat(taskQueue.peek()).isNull();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(2);
+    }
+
+    @Test
+    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);
+
+        mDataBroker.scheduleNextTask();
+        waitForHandlerThreadToFinish();
+        mFakeScriptExecutor.notifyScriptSuccess(mData); // posts to telemetry handler
+
+        waitForHandlerThreadToFinish();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+        verify(mMockResultStore).putInterimResult(
+                eq(mHighPriorityTask.getMetricsConfig().getName()), eq(mData));
+    }
+
+    @Test
+    public void testScheduleNextTask_onScriptError_shouldStoreErrorObject() throws Exception {
+        mDataBroker.getTaskQueue().add(mHighPriorityTask);
+        TelemetryProto.TelemetryError.ErrorType errorType =
+                TelemetryProto.TelemetryError.ErrorType.LUA_RUNTIME_ERROR;
+        String errorMessage = "test onError";
+        TelemetryProto.TelemetryError expectedError = TelemetryProto.TelemetryError.newBuilder()
+                .setErrorType(errorType)
+                .setMessage(errorMessage)
+                .build();
+
+        mDataBroker.scheduleNextTask();
+        waitForHandlerThreadToFinish();
+        mFakeScriptExecutor.notifyScriptError(errorType.getNumber(), errorMessage);
+
+        waitForHandlerThreadToFinish();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+        verify(mMockResultStore).putError(eq(METRICS_CONFIG_FOO.getName()), eq(expectedError));
+    }
+
+    @Test
+    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_bindScriptExecutorFailedOnce_shouldRebind()
+            throws Exception {
+        Mockito.reset(mMockContext);
+        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(
+                new Answer() {
+                    private int mCount = 0;
+
+                    @Override
+                    public Object answer(InvocationOnMock invocation) {
+                        if (mCount++ == 1) {
+                            return false; // fail first attempt
+                        }
+                        ServiceConnection conn = invocation.getArgument(1);
+                        conn.onServiceConnected(null, mMockScriptExecutorBinder);
+                        return true; // second attempt should succeed
+                    }
+                });
+        mDataBroker.mBindScriptExecutorDelayMillis = 0L; // immediately rebind for testing purpose
+        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
+        PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+        taskQueue.add(mHighPriorityTask);
+
+        // will rebind to ScriptExecutor if it is null
+        mDataBroker.scheduleNextTask();
+
+        waitForHandlerThreadToFinish();
+        assertThat(taskQueue.peek()).isNull();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testScheduleNextTask_bindScriptExecutorFailedMultipleTimes_shouldDisableBroker()
+            throws Exception {
+        // fail 6 future attempts to bind to it
+        Mockito.reset(mMockContext);
+        when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any()))
+                .thenReturn(false, false, false, false, false, false);
+        mDataBroker.mBindScriptExecutorDelayMillis = 0L; // immediately rebind for testing purpose
+        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
+        PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+        taskQueue.add(mHighPriorityTask);
+
+        // will rebind to ScriptExecutor if it is null
+        mDataBroker.scheduleNextTask();
+
+        waitForHandlerThreadToFinish();
+        // broker disabled, all subscribers should have been removed
+        assertThat(mDataBroker.getSubscriptionMap()).hasSize(0);
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testScheduleNextTask_whenScriptExecutorThrowsException_shouldRequeueTask()
+            throws Exception {
+        PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+        taskQueue.add(mHighPriorityTask);
+        mFakeScriptExecutor.failNextApiCalls(1); // fail the next invokeScript() call
+
+        mDataBroker.scheduleNextTask();
+
+        waitForHandlerThreadToFinish();
+        // expect invokeScript() to be called and failed, causing the same task to be re-queued
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+        assertThat(taskQueue.peek()).isEqualTo(mHighPriorityTask);
+    }
+
+    @Test
+    public void testAddTaskToQueue_shouldInvokeScriptExecutor() throws Exception {
+        mDataBroker.addTaskToQueue(mHighPriorityTask);
+
+        waitForHandlerThreadToFinish();
+        assertThat(mFakeScriptExecutor.getApiInvocationCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testAddMetricsConfiguration_newMetricsConfig() {
+        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_BAR);
+
+        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
+        assertThat(mDataBroker.getSubscriptionMap().get(METRICS_CONFIG_BAR.getName())).hasSize(1);
+    }
+
+
+    @Test
+    public void testAddMetricsConfiguration_duplicateMetricsConfig_shouldDoNothing() {
+        // this metrics config has already been added in setUp()
+        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
+
+        assertThat(mDataBroker.getSubscriptionMap()).hasSize(1);
+        assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_FOO.getName());
+        assertThat(mDataBroker.getSubscriptionMap().get(METRICS_CONFIG_FOO.getName())).hasSize(1);
+    }
+
+    @Test
+    public void testRemoveMetricsConfiguration_shouldRemoveAllAssociatedTasks() {
+        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
+        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_BAR);
+        ScriptExecutionTask taskWithMetricsConfigFoo = new ScriptExecutionTask(
+                new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
+                mData,
+                SystemClock.elapsedRealtime());
+        PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+        taskQueue.add(mHighPriorityTask); // associated with METRICS_CONFIG_FOO
+        taskQueue.add(mLowPriorityTask); // associated with METRICS_CONFIG_BAR
+        taskQueue.add(taskWithMetricsConfigFoo); // associated with METRICS_CONFIG_FOO
+        assertThat(taskQueue).hasSize(3);
+
+        mDataBroker.removeMetricsConfiguration(METRICS_CONFIG_FOO);
+
+        assertThat(taskQueue).hasSize(1);
+        assertThat(taskQueue.poll()).isEqualTo(mLowPriorityTask);
+    }
+
+    @Test
+    public void testRemoveMetricsConfiguration_whenMetricsConfigNonExistent_shouldDoNothing() {
+        mDataBroker.removeMetricsConfiguration(METRICS_CONFIG_BAR);
+
+        assertThat(mDataBroker.getSubscriptionMap()).hasSize(0);
+    }
+
+    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
+    }
+
+    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,
+                PersistableBundle publishedData, @Nullable PersistableBundle savedState,
+                IScriptExecutorListener listener)
+                throws RemoteException {
+            mApiInvocationCount++;
+            mSavedState = savedState;
+            mListener = listener;
+            if (mFailApi > 0) {
+                mFailApi--;
+                throw new RemoteException("Simulated failure");
+            }
+        }
+
+        @Override
+        public void invokeScriptForLargeInput(String scriptBody, String functionName,
+                ParcelFileDescriptor publishedDataFileDescriptor,
+                @Nullable PersistableBundle savedState,
+                IScriptExecutorListener listener) throws RemoteException {
+            mApiInvocationCount++;
+            mSavedState = savedState;
+            mListener = listener;
+            if (mFailApi > 0) {
+                mFailApi--;
+                throw new RemoteException("Simulated failure");
+            }
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+
+        /** Mocks script temporary completion. */
+        public void notifyScriptSuccess(PersistableBundle bundle) {
+            try {
+                mListener.onSuccess(bundle);
+            } catch (RemoteException e) {
+                // nothing to do
+            }
+        }
+
+        /** Mocks script producing final result. */
+        public void notifyScriptFinish(PersistableBundle bundle) {
+            try {
+                mListener.onScriptFinished(bundle);
+            } catch (RemoteException e) {
+                // nothing to do
+            }
+        }
+
+        /** Mocks script finished with error. */
+        public void notifyScriptError(int errorType, String errorMessage) {
+            try {
+                mListener.onError(errorType, errorMessage, null);
+            } catch (RemoteException e) {
+                // nothing to do
+            }
+        }
+
+        /** Fails the next N invokeScript() call. */
+        public void failNextApiCalls(int n) {
+            mFailApi = n;
+        }
+
+        /** Returns number of times the ScriptExecutor API was invoked. */
+        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/databroker/DataBrokerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerUnitTest.java
deleted file mode 100644
index 45c5a22..0000000
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerUnitTest.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.telemetry.databroker;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.car.hardware.CarPropertyConfig;
-
-import com.android.car.CarPropertyService;
-import com.android.car.telemetry.TelemetryProto;
-import com.android.car.telemetry.publisher.PublisherFactory;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-import java.util.Collections;
-
-@RunWith(MockitoJUnitRunner.class)
-public class DataBrokerUnitTest {
-    private static final int PROP_ID = 100;
-    private static final int PROP_AREA = 200;
-    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 TelemetryProto.VehiclePropertyPublisher
-            VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION =
-            TelemetryProto.VehiclePropertyPublisher.newBuilder().setReadRate(
-                    1).setVehiclePropertyId(PROP_ID).build();
-    private static final TelemetryProto.Publisher PUBLISHER_CONFIGURATION =
-            TelemetryProto.Publisher.newBuilder().setVehicleProperty(
-                    VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION).build();
-    private static final TelemetryProto.Subscriber SUBSCRIBER_FOO =
-            TelemetryProto.Subscriber.newBuilder().setHandler("function_name_foo").setPublisher(
-                    PUBLISHER_CONFIGURATION).build();
-    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_FOO =
-            TelemetryProto.MetricsConfig.newBuilder().setName("Foo").setVersion(
-                    1).addSubscribers(SUBSCRIBER_FOO).build();
-    private static final TelemetryProto.Subscriber SUBSCRIBER_BAR =
-            TelemetryProto.Subscriber.newBuilder().setHandler("function_name_bar").setPublisher(
-                    PUBLISHER_CONFIGURATION).build();
-    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_BAR =
-            TelemetryProto.MetricsConfig.newBuilder().setName("Bar").setVersion(
-                    1).addSubscribers(SUBSCRIBER_BAR).build();
-
-    @Mock
-    private CarPropertyService mMockCarPropertyService;
-
-    private DataBrokerImpl mDataBroker;
-
-    @Before
-    public void setUp() {
-        when(mMockCarPropertyService.getPropertyList())
-                .thenReturn(Collections.singletonList(PROP_CONFIG));
-        PublisherFactory factory = new PublisherFactory(mMockCarPropertyService);
-        mDataBroker = new DataBrokerImpl(factory);
-    }
-
-    @Test
-    public void testAddMetricsConfiguration_newMetricsConfig() {
-        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
-
-        assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_FOO.getName());
-        // there should be one data subscriber in the subscription list of METRICS_CONFIG_FOO
-        assertThat(mDataBroker.getSubscriptionMap().get(METRICS_CONFIG_FOO.getName())).hasSize(1);
-    }
-
-    @Test
-    public void testAddMetricsConfiguration_multipleMetricsConfigsSamePublisher() {
-        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
-        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_BAR);
-
-        assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_FOO.getName());
-        assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_BAR.getName());
-    }
-
-    @Test
-    public void testAddMetricsConfiguration_addSameMetricsConfigs() {
-        mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
-
-        boolean status = mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
-
-        assertThat(status).isFalse();
-    }
-
-    @Test
-    public void testRemoveMetricsConfiguration_removeNonexistentMetricsConfig() {
-        boolean status = mDataBroker.removeMetricsConfiguration(METRICS_CONFIG_FOO);
-
-        assertThat(status).isFalse();
-    }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataSubscriberTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataSubscriberTest.java
new file mode 100644
index 0000000..69602f1
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataSubscriberTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.databroker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.car.telemetry.TelemetryProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DataSubscriberTest {
+
+    private static final TelemetryProto.VehiclePropertyPublisher
+            VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION =
+            TelemetryProto.VehiclePropertyPublisher.newBuilder().setReadRate(
+                    1).setVehiclePropertyId(100).build();
+    private static final TelemetryProto.Publisher PUBLISHER_CONFIGURATION =
+            TelemetryProto.Publisher.newBuilder().setVehicleProperty(
+                    VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION).build();
+    private static final TelemetryProto.Subscriber SUBSCRIBER_FOO =
+            TelemetryProto.Subscriber.newBuilder().setHandler("function_name_foo").setPublisher(
+                    PUBLISHER_CONFIGURATION).setPriority(1).build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_FOO =
+            TelemetryProto.MetricsConfig.newBuilder().setName("Foo").setVersion(
+                    1).addSubscribers(SUBSCRIBER_FOO).build();
+    private static final TelemetryProto.Subscriber SUBSCRIBER_BAR =
+            TelemetryProto.Subscriber.newBuilder().setHandler("function_name_bar").setPublisher(
+                    PUBLISHER_CONFIGURATION).setPriority(1).build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG_BAR =
+            TelemetryProto.MetricsConfig.newBuilder().setName("Bar").setVersion(
+                    1).addSubscribers(SUBSCRIBER_BAR).build();
+
+    @Mock
+    private DataBroker mMockDataBroker;
+
+    @Test
+    public void testEquals_whenSame_shouldBeEqual() {
+        DataSubscriber foo = new DataSubscriber(mMockDataBroker, METRICS_CONFIG_FOO,
+                SUBSCRIBER_FOO);
+        DataSubscriber bar = new DataSubscriber(mMockDataBroker, METRICS_CONFIG_FOO,
+                SUBSCRIBER_FOO);
+
+        assertThat(foo).isEqualTo(bar);
+    }
+
+    @Test
+    public void testEquals_whenDifferent_shouldNotBeEqual() {
+        DataSubscriber foo = new DataSubscriber(mMockDataBroker, METRICS_CONFIG_FOO,
+                SUBSCRIBER_FOO);
+        DataSubscriber bar = new DataSubscriber(mMockDataBroker, METRICS_CONFIG_BAR,
+                SUBSCRIBER_BAR);
+
+        assertThat(foo).isNotEqualTo(bar);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/AtomDataConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/AtomDataConverterTest.java
new file mode 100644
index 0000000..c61c106
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/AtomDataConverterTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.publisher;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class AtomDataConverterTest {
+    @Test
+    public void testConvertPushedAtomsListWithUnsetFields_putsCorrectDataToPersistableBundle() {
+        List<AtomsProto.Atom> pushedAtomsList = Arrays.asList(
+                AtomsProto.Atom.newBuilder()
+                        .setAppStartMemoryStateCaptured(
+                                AtomsProto.AppStartMemoryStateCaptured.newBuilder()
+                                        .setUid(1000)
+                                        .setActivityName("activityName1")
+                                        .setRssInBytes(1234L))
+                        .build(),
+                AtomsProto.Atom.newBuilder()
+                        .setAppStartMemoryStateCaptured(
+                                AtomsProto.AppStartMemoryStateCaptured.newBuilder()
+                                        .setUid(1100)
+                                        .setActivityName("activityName2")
+                                        .setRssInBytes(2345L))
+                        .build()
+        );
+        PersistableBundle bundle = new PersistableBundle();
+
+        AtomDataConverter.convertAtomsList(pushedAtomsList, bundle);
+
+        assertThat(bundle.size()).isEqualTo(3);
+        assertThat(bundle.getIntArray(AtomDataConverter.UID))
+            .asList().containsExactly(1000, 1100).inOrder();
+        assertThat(Arrays.asList(bundle.getStringArray(AtomDataConverter.ACTIVITY_NAME)))
+            .containsExactly("activityName1", "activityName2").inOrder();
+        assertThat(bundle.getLongArray(AtomDataConverter.RSS_IN_BYTES))
+            .asList().containsExactly(1234L, 2345L).inOrder();
+    }
+
+    @Test
+    public void testConvertPulledAtomsListWithUnsetFields_putsCorrectDataToPersistableBundle() {
+        List<AtomsProto.Atom> pulledAtomsList = Arrays.asList(
+                AtomsProto.Atom.newBuilder()
+                        .setProcessMemoryState(AtomsProto.ProcessMemoryState.newBuilder()
+                                .setUid(1000)
+                                .setProcessName("processName1")
+                                .setRssInBytes(1234L))
+                        .build(),
+                AtomsProto.Atom.newBuilder()
+                        .setProcessMemoryState(AtomsProto.ProcessMemoryState.newBuilder()
+                                .setUid(1100)
+                                .setProcessName("processName2")
+                                .setRssInBytes(2345L))
+                        .build()
+        );
+        PersistableBundle bundle = new PersistableBundle();
+
+        AtomDataConverter.convertAtomsList(pulledAtomsList, bundle);
+
+        assertThat(bundle.size()).isEqualTo(3);
+        assertThat(bundle.getIntArray(AtomDataConverter.UID))
+            .asList().containsExactly(1000, 1100).inOrder();
+        assertThat(Arrays.asList(bundle.getStringArray(AtomDataConverter.PROCESS_NAME)))
+            .containsExactly("processName1", "processName2").inOrder();
+        assertThat(bundle.getLongArray(AtomDataConverter.RSS_IN_BYTES))
+            .asList().containsExactly(1234L, 2345L).inOrder();
+    }
+
+    @Test
+    public void testConvertAppStartMemoryStateCapturedAtoms_putsCorrectDataToPersistableBundle() {
+        List<AtomsProto.Atom> atomsList = Arrays.asList(
+                AtomsProto.Atom.newBuilder()
+                        .setAppStartMemoryStateCaptured(
+                                AtomsProto.AppStartMemoryStateCaptured.newBuilder()
+                                        .setUid(1000)
+                                        .setProcessName("processName")
+                                        .setActivityName("activityName")
+                                        .setPageFault(59L)
+                                        .setPageMajorFault(34L)
+                                        .setRssInBytes(1234L)
+                                        .setCacheInBytes(234L)
+                                        .setSwapInBytes(111L))
+                        .build()
+        );
+        PersistableBundle bundle = new PersistableBundle();
+
+        AtomDataConverter.convertAtomsList(atomsList, bundle);
+
+        assertThat(bundle.size()).isEqualTo(8);
+        assertThat(bundle.getIntArray(AtomDataConverter.UID)).asList().containsExactly(1000);
+        assertThat(Arrays.asList(bundle.getStringArray(AtomDataConverter.PROCESS_NAME)))
+            .containsExactly("processName");
+        assertThat(Arrays.asList(bundle.getStringArray(AtomDataConverter.ACTIVITY_NAME)))
+            .containsExactly("activityName");
+        assertThat(bundle.getLongArray(AtomDataConverter.PAGE_FAULT))
+            .asList().containsExactly(59L);
+        assertThat(bundle.getLongArray(AtomDataConverter.PAGE_MAJOR_FAULT))
+            .asList().containsExactly(34L);
+        assertThat(bundle.getLongArray(AtomDataConverter.RSS_IN_BYTES))
+            .asList().containsExactly(1234L);
+        assertThat(bundle.getLongArray(AtomDataConverter.CACHE_IN_BYTES))
+            .asList().containsExactly(234L);
+        assertThat(bundle.getLongArray(AtomDataConverter.SWAP_IN_BYTES))
+            .asList().containsExactly(111L);
+    }
+
+    @Test
+    public void testConvertProcessMemoryStateAtoms_putsCorrectDataToPersistableBundle() {
+        List<AtomsProto.Atom> atomsList = Arrays.asList(
+                AtomsProto.Atom.newBuilder()
+                        .setProcessMemoryState(AtomsProto.ProcessMemoryState.newBuilder()
+                                .setUid(1000)
+                                .setProcessName("processName")
+                                .setOomAdjScore(100)
+                                .setPageFault(59L)
+                                .setPageMajorFault(34L)
+                                .setRssInBytes(1234L)
+                                .setCacheInBytes(234L)
+                                .setSwapInBytes(111L))
+                        .build()
+        );
+        PersistableBundle bundle = new PersistableBundle();
+
+        AtomDataConverter.convertAtomsList(atomsList, bundle);
+
+        assertThat(bundle.size()).isEqualTo(8);
+        assertThat(bundle.getIntArray(AtomDataConverter.UID)).asList().containsExactly(1000);
+        assertThat(Arrays.asList(bundle.getStringArray(AtomDataConverter.PROCESS_NAME)))
+            .containsExactly("processName");
+        assertThat(bundle.getIntArray(AtomDataConverter.OOM_ADJ_SCORE))
+            .asList().containsExactly(100);
+        assertThat(bundle.getLongArray(AtomDataConverter.PAGE_FAULT))
+            .asList().containsExactly(59L);
+        assertThat(bundle.getLongArray(AtomDataConverter.PAGE_MAJOR_FAULT))
+            .asList().containsExactly(34L);
+        assertThat(bundle.getLongArray(AtomDataConverter.RSS_IN_BYTES))
+            .asList().containsExactly(1234L);
+        assertThat(bundle.getLongArray(AtomDataConverter.CACHE_IN_BYTES))
+            .asList().containsExactly(234L);
+        assertThat(bundle.getLongArray(AtomDataConverter.SWAP_IN_BYTES))
+            .asList().containsExactly(111L);
+    }
+}
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
new file mode 100644
index 0000000..bc3cd47
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/CarTelemetrydPublisherTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.publisher;
+
+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;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+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;
+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;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CarTelemetrydPublisherTest extends AbstractExtendedMockitoTestCase {
+    private static final String SERVICE_NAME = ICarTelemetryInternal.DESCRIPTOR + "/default";
+    private static final int CAR_DATA_ID_1 = 1;
+    private static final TelemetryProto.Publisher PUBLISHER_PARAMS_1 =
+            TelemetryProto.Publisher.newBuilder()
+                    .setCartelemetryd(TelemetryProto.CarTelemetrydPublisher.newBuilder()
+                            .setId(CAR_DATA_ID_1))
+                    .build();
+
+    private final FakeHandlerWrapper mFakeHandlerWrapper =
+            new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.IMMEDIATE);
+
+    @Mock private IBinder mMockBinder;
+    @Mock private DataSubscriber mMockDataSubscriber;
+
+    @Captor private ArgumentCaptor<IBinder.DeathRecipient> mLinkToDeathCallbackCaptor;
+
+    @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);
+        when(mMockBinder.queryLocalInterface(any())).thenReturn(mFakeCarTelemetryInternal);
+        doNothing().when(mMockBinder).linkToDeath(mLinkToDeathCallbackCaptor.capture(), anyInt());
+        doReturn(mMockBinder).when(() -> ServiceManager.checkService(SERVICE_NAME));
+    }
+
+    @Override
+    protected void onSessionBuilder(CustomMockitoSessionBuilder builder) {
+        builder.spyStatic(ServiceManager.class);
+    }
+
+    @Test
+    public void testAddDataSubscriber_registersNewListener() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        assertThat(mFakeCarTelemetryInternal.mListener).isNotNull();
+        assertThat(mPublisher.isConnectedToCarTelemetryd()).isTrue();
+        assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+    }
+
+    @Test
+    public void testAddDataSubscriber_withInvalidId_fails() {
+        DataSubscriber invalidDataSubscriber = Mockito.mock(DataSubscriber.class);
+        when(invalidDataSubscriber.getPublisherParam()).thenReturn(
+                TelemetryProto.Publisher.newBuilder()
+                        .setCartelemetryd(TelemetryProto.CarTelemetrydPublisher.newBuilder()
+                                .setId(42000))  // invalid ID
+                        .build());
+
+        Throwable error = assertThrows(IllegalArgumentException.class,
+                () -> mPublisher.addDataSubscriber(invalidDataSubscriber));
+
+        assertThat(error).hasMessageThat().contains("Invalid CarData ID");
+        assertThat(mFakeCarTelemetryInternal.mListener).isNull();
+        assertThat(mPublisher.isConnectedToCarTelemetryd()).isFalse();
+        assertThat(mPublisher.hasDataSubscriber(invalidDataSubscriber)).isFalse();
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_ignoresIfNotFound() {
+        mPublisher.removeDataSubscriber(mMockDataSubscriber);
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_removesOnlySingleSubscriber() {
+        DataSubscriber subscriber2 = Mockito.mock(DataSubscriber.class);
+        when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        mPublisher.addDataSubscriber(subscriber2);
+
+        mPublisher.removeDataSubscriber(subscriber2);
+
+        assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+        assertThat(mPublisher.hasDataSubscriber(subscriber2)).isFalse();
+        assertThat(mFakeCarTelemetryInternal.mListener).isNotNull();
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_disconnectsFromICarTelemetry() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        mPublisher.removeDataSubscriber(mMockDataSubscriber);
+
+        assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+        assertThat(mFakeCarTelemetryInternal.mListener).isNull();
+    }
+
+    @Test
+    public void testRemoveAllDataSubscribers_succeeds() {
+        DataSubscriber subscriber2 = Mockito.mock(DataSubscriber.class);
+        when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        mPublisher.addDataSubscriber(subscriber2);
+
+        mPublisher.removeAllDataSubscribers();
+
+        assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+        assertThat(mPublisher.hasDataSubscriber(subscriber2)).isFalse();
+        assertThat(mFakeCarTelemetryInternal.mListener).isNull();
+    }
+
+    @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/ConfigMetricsReportListConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/ConfigMetricsReportListConverterTest.java
new file mode 100644
index 0000000..367fc5c
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/ConfigMetricsReportListConverterTest.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.telemetry.publisher;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto;
+import com.android.car.telemetry.StatsLogProto.ConfigMetricsReportList;
+import com.android.car.telemetry.StatsLogProto.StatsLogReport;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class ConfigMetricsReportListConverterTest {
+    @Test
+    public void testConvertMultipleReports_correctlyGroupsByMetricId() {
+        StatsLogProto.EventMetricData eventData = StatsLogProto.EventMetricData.newBuilder()
+                .setElapsedTimestampNanos(99999999L)
+                .setAtom(AtomsProto.Atom.newBuilder()
+                        .setAppStartMemoryStateCaptured(
+                                AtomsProto.AppStartMemoryStateCaptured.newBuilder()
+                                        .setUid(1000)
+                                        .setActivityName("activityName")
+                                        .setRssInBytes(1234L)))
+                .build();
+
+        StatsLogProto.GaugeMetricData gaugeData = StatsLogProto.GaugeMetricData.newBuilder()
+                .addBucketInfo(StatsLogProto.GaugeBucketInfo.newBuilder()
+                        .addAtom(AtomsProto.Atom.newBuilder()
+                                .setProcessMemoryState(AtomsProto.ProcessMemoryState.newBuilder()
+                                        .setUid(1300)
+                                        .setProcessName("processName")
+                                        .setRssInBytes(4567L)))
+                        .addElapsedTimestampNanos(445678901L))
+                .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+                        .setValueInt(234))
+                .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+                        .setValueStrHash(345678901L))
+                .build();
+
+        ConfigMetricsReportList reportList = ConfigMetricsReportList.newBuilder()
+                .addReports(StatsLogProto.ConfigMetricsReport.newBuilder()
+                        .addMetrics(StatsLogReport.newBuilder()
+                                .setMetricId(12345L)
+                                .setEventMetrics(
+                                        StatsLogReport.EventMetricDataWrapper.newBuilder()
+                                                .addData(eventData))))
+                .addReports(StatsLogProto.ConfigMetricsReport.newBuilder()
+                        .addMetrics(StatsLogProto.StatsLogReport.newBuilder()
+                                .setMetricId(23456L)
+                                .setGaugeMetrics(
+                                        StatsLogReport.GaugeMetricDataWrapper.newBuilder()
+                                                .addData(gaugeData))))
+                .build();
+
+        Map<Long, PersistableBundle> map = ConfigMetricsReportListConverter.convert(reportList);
+
+        PersistableBundle subBundle1 = map.get(12345L);
+        PersistableBundle subBundle2 = map.get(23456L);
+        PersistableBundle dimensionBundle = subBundle2.getPersistableBundle("234-345678901");
+        assertThat(new ArrayList<Long>(map.keySet())).containsExactly(12345L, 23456L);
+        assertThat(subBundle1.getLongArray(EventMetricDataConverter.ELAPSED_TIME_NANOS))
+            .asList().containsExactly(99999999L);
+        assertThat(subBundle1.getIntArray(AtomDataConverter.UID)).asList().containsExactly(1000);
+        assertThat(Arrays.asList(subBundle1.getStringArray(AtomDataConverter.ACTIVITY_NAME)))
+            .containsExactly("activityName");
+        assertThat(subBundle1.getLongArray(AtomDataConverter.RSS_IN_BYTES))
+            .asList().containsExactly(1234L);
+        // TODO(b/200064146) add checks for uid and process_name
+        assertThat(subBundle2.getLongArray(AtomDataConverter.RSS_IN_BYTES))
+            .asList().containsExactly(4567L);
+        assertThat(subBundle2.getLongArray(EventMetricDataConverter.ELAPSED_TIME_NANOS))
+            .asList().containsExactly(445678901L);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/EventMetricDataConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/EventMetricDataConverterTest.java
new file mode 100644
index 0000000..2f4f939
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/EventMetricDataConverterTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.publisher;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class EventMetricDataConverterTest {
+    @Test
+    public void testConvertEventDataList_putsCorrectDataIntoPersistableBundle() {
+        List<StatsLogProto.EventMetricData> eventDataList = Arrays.asList(
+                StatsLogProto.EventMetricData.newBuilder()
+                        .setElapsedTimestampNanos(12345678L)
+                        .setAtom(AtomsProto.Atom.newBuilder()
+                                .setAppStartMemoryStateCaptured(
+                                        AtomsProto.AppStartMemoryStateCaptured.newBuilder()
+                                                .setUid(1000)
+                                                .setActivityName("activityName1")
+                                                .setRssInBytes(1234L)))
+                        .build(),
+                StatsLogProto.EventMetricData.newBuilder()
+                        .setElapsedTimestampNanos(23456789L)
+                        .setAtom(AtomsProto.Atom.newBuilder()
+                                .setAppStartMemoryStateCaptured(
+                                        AtomsProto.AppStartMemoryStateCaptured.newBuilder()
+                                                .setUid(1100)
+                                                .setActivityName("activityName2")
+                                                .setRssInBytes(2345L)))
+                        .build()
+        );
+        PersistableBundle bundle = new PersistableBundle();
+        EventMetricDataConverter.convertEventDataList(eventDataList, bundle);
+
+        assertThat(bundle.size()).isEqualTo(4);
+        assertThat(bundle.getLongArray(EventMetricDataConverter.ELAPSED_TIME_NANOS))
+            .asList().containsExactly(12345678L, 23456789L);
+        assertThat(bundle.getIntArray(AtomDataConverter.UID))
+            .asList().containsExactly(1000, 1100);
+        assertThat(Arrays.asList(bundle.getStringArray(AtomDataConverter.ACTIVITY_NAME)))
+            .containsExactly("activityName1", "activityName2");
+        assertThat(bundle.getLongArray(AtomDataConverter.RSS_IN_BYTES))
+            .asList().containsExactly(1234L, 2345L);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/GaugeMetricDataConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/GaugeMetricDataConverterTest.java
new file mode 100644
index 0000000..7ee19ce
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/GaugeMetricDataConverterTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.publisher;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class GaugeMetricDataConverterTest {
+    @Test
+    public void testConvertGaugeDataList_putsCorrectDataIntoPersistableBundle() {
+        // TODO(b/200064146): handle process name and uid
+        List<StatsLogProto.GaugeMetricData> gaugeDataList = Arrays.asList(
+                StatsLogProto.GaugeMetricData.newBuilder()
+                        .addBucketInfo(StatsLogProto.GaugeBucketInfo.newBuilder()
+                                .addAtom(AtomsProto.Atom.newBuilder()
+                                        .setProcessMemoryState(
+                                                AtomsProto.ProcessMemoryState.newBuilder()
+                                                    .setUid(1000)
+                                                    .setProcessName("processName1")
+                                                    .setRssInBytes(1234L)))
+                                .addElapsedTimestampNanos(12345678L)
+                                .addAtom(AtomsProto.Atom.newBuilder()
+                                        .setProcessMemoryState(
+                                                AtomsProto.ProcessMemoryState.newBuilder()
+                                                    .setUid(1000)
+                                                    .setProcessName("processName1")
+                                                    .setRssInBytes(2345L)))
+                                .addElapsedTimestampNanos(23456789L))
+                        .addBucketInfo(StatsLogProto.GaugeBucketInfo.newBuilder()
+                                .addAtom(AtomsProto.Atom.newBuilder()
+                                        .setProcessMemoryState(
+                                                AtomsProto.ProcessMemoryState.newBuilder()
+                                                    .setUid(1200)
+                                                    .setProcessName("processName2")
+                                                    .setRssInBytes(3456L)))
+                                .addElapsedTimestampNanos(34567890L))
+                        .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+                                .setValueInt(123))
+                        .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+                                .setValueStrHash(234567890L))
+                        .build(),
+                StatsLogProto.GaugeMetricData.newBuilder()
+                        .addBucketInfo(StatsLogProto.GaugeBucketInfo.newBuilder()
+                                .addAtom(AtomsProto.Atom.newBuilder()
+                                        .setProcessMemoryState(
+                                                AtomsProto.ProcessMemoryState.newBuilder()
+                                                    .setUid(1300)
+                                                    .setProcessName("processName3")
+                                                    .setRssInBytes(4567L)))
+                                .addElapsedTimestampNanos(445678901L))
+                        .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+                                .setValueInt(234))
+                        .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+                                .setValueStrHash(345678901L))
+                        .build()
+        );
+        PersistableBundle bundle = new PersistableBundle();
+
+        GaugeMetricDataConverter.convertGaugeDataList(gaugeDataList, bundle);
+
+        assertThat(bundle.getLongArray(AtomDataConverter.RSS_IN_BYTES))
+            .asList().containsExactly(1234L, 2345L, 3456L, 4567L);
+        assertThat(bundle.getLongArray(EventMetricDataConverter.ELAPSED_TIME_NANOS))
+            .asList().containsExactly(12345678L, 23456789L, 34567890L, 445678901L);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/HashUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/HashUtilsTest.java
new file mode 100644
index 0000000..1be835d
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/HashUtilsTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.publisher;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class HashUtilsTest {
+    @Test
+    public void testSha256() {
+        assertThat(HashUtils.sha256("")).isEqualTo(1449310910991872227L);
+        assertThat(HashUtils.sha256("a")).isEqualTo(-3837880752741967926L);
+        assertThat(HashUtils.sha256("aa")).isEqualTo(-8157175689457624170L);
+        assertThat(HashUtils.sha256("b")).isEqualTo(5357375904281011006L);
+    }
+}
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
new file mode 100644
index 0000000..5b7e21e
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
@@ -0,0 +1,348 @@
+/*
+ * 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.publisher;
+
+import static com.android.car.telemetry.AtomsProto.Atom.APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER;
+import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.APP_START_MEMORY_STATE_CAPTURED;
+import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.PROCESS_MEMORY_STATE;
+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.PROCESS_MEMORY_STATE_FIELDS_MATCHER;
+import static com.android.car.telemetry.publisher.StatsPublisher.PROCESS_MEMORY_STATE_GAUGE_METRIC_ID;
+import static com.android.car.telemetry.publisher.StatsPublisher.PROCESS_MEMORY_STATE_MATCHER_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.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.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.file.Files;
+
+@RunWith(MockitoJUnitRunner.class)
+public class StatsPublisherTest {
+    private static final TelemetryProto.Publisher STATS_PUBLISHER_PARAMS_1 =
+            TelemetryProto.Publisher.newBuilder()
+                    .setStats(TelemetryProto.StatsPublisher.newBuilder()
+                            .setSystemMetric(APP_START_MEMORY_STATE_CAPTURED))
+                    .build();
+    private static final TelemetryProto.Publisher STATS_PUBLISHER_PARAMS_2 =
+            TelemetryProto.Publisher.newBuilder()
+                    .setStats(TelemetryProto.StatsPublisher.newBuilder()
+                            .setSystemMetric(PROCESS_MEMORY_STATE))
+                    .build();
+    private static final TelemetryProto.Subscriber SUBSCRIBER_1 =
+            TelemetryProto.Subscriber.newBuilder()
+                    .setHandler("handler_fn_1")
+                    .setPublisher(STATS_PUBLISHER_PARAMS_1)
+                    .build();
+    private static final TelemetryProto.Subscriber SUBSCRIBER_2 =
+            TelemetryProto.Subscriber.newBuilder()
+                    .setHandler("handler_fn_2")
+                    .setPublisher(STATS_PUBLISHER_PARAMS_2)
+                    .build();
+    private static final TelemetryProto.MetricsConfig METRICS_CONFIG =
+            TelemetryProto.MetricsConfig.newBuilder()
+                    .setName("myconfig")
+                    .setVersion(1)
+                    .addSubscribers(SUBSCRIBER_1)
+                    .addSubscribers(SUBSCRIBER_2)
+                    .build();
+
+    private static final long SUBSCRIBER_1_HASH = -8101507323446050791L;  // Used as ID.
+    private static final long SUBSCRIBER_2_HASH = 2778197004730583271L;  // 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(
+                                                    APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER)))
+                    .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 StatsdConfigProto.StatsdConfig STATSD_CONFIG_2 =
+            StatsdConfigProto.StatsdConfig.newBuilder()
+                    .setId(SUBSCRIBER_2_HASH)
+                    .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+                            // The id must be unique within StatsdConfig/matchers
+                            .setId(PROCESS_MEMORY_STATE_MATCHER_ID)
+                            .setSimpleAtomMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+                                    .setAtomId(PROCESS_MEMORY_STATE_FIELD_NUMBER)))
+                    .addGaugeMetric(StatsdConfigProto.GaugeMetric.newBuilder()
+                            // The id must be unique within StatsdConfig/metrics
+                            .setId(PROCESS_MEMORY_STATE_GAUGE_METRIC_ID)
+                            .setWhat(PROCESS_MEMORY_STATE_MATCHER_ID)
+                            .setDimensionsInWhat(StatsdConfigProto.FieldMatcher.newBuilder()
+                                    .setField(PROCESS_MEMORY_STATE_FIELD_NUMBER)
+                                    .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                                            .setField(1))  // ProcessMemoryState.uid
+                                    .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+                                            .setField(2))  // ProcessMemoryState.process_name
+                            )
+                            .setGaugeFieldsFilter(StatsdConfigProto.FieldFilter.newBuilder()
+                                    .setFields(PROCESS_MEMORY_STATE_FIELDS_MATCHER))
+                            .setSamplingType(
+                                    StatsdConfigProto.GaugeMetric.SamplingType.RANDOM_ONE_SAMPLE)
+                            .setBucket(StatsdConfigProto.TimeUnit.FIVE_MINUTES)
+                    )
+                    .addAllowedLogSource("AID_SYSTEM")
+                    .addPullAtomPackages(StatsdConfigProto.PullAtomPackages.newBuilder()
+                        .setAtomId(PROCESS_MEMORY_STATE_FIELD_NUMBER)
+                        .addPackages("AID_SYSTEM"))
+                    .build();
+
+    private static final StatsLogProto.ConfigMetricsReportList EMPTY_STATS_REPORT =
+            StatsLogProto.ConfigMetricsReportList.newBuilder().build();
+
+    private static final DataSubscriber DATA_SUBSCRIBER_1 =
+            new DataSubscriber(null, METRICS_CONFIG, SUBSCRIBER_1);
+
+    private final FakeHandlerWrapper mFakeHandlerWrapper =
+            new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.QUEUEING);
+
+    private File mRootDirectory;
+    private StatsPublisher mPublisher;  // subject
+    private Throwable mPublisherFailure;
+
+    @Mock private StatsManagerProxy mStatsManager;
+
+    @Captor private ArgumentCaptor<PersistableBundle> mBundleCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        mRootDirectory = Files.createTempDirectory("telemetry_test").toFile();
+        mPublisher = createRestartedPublisher();
+    }
+
+    /**
+     * Emulates a restart by creating a new StatsPublisher. StatsManager and PersistableBundle
+     * stays the same.
+     */
+    private StatsPublisher createRestartedPublisher() throws Exception {
+        return new StatsPublisher(
+                this::onPublisherFailure,
+                mStatsManager,
+                mRootDirectory,
+                mFakeHandlerWrapper.getMockHandler());
+    }
+
+    @Test
+    public void testAddDataSubscriber_registersNewListener() throws Exception {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+        verify(mStatsManager, times(1))
+                .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
+        assertThat(mPublisher.hasDataSubscriber(DATA_SUBSCRIBER_1)).isTrue();
+    }
+
+    @Test
+    public void testAddDataSubscriber_sameVersion_addsToStatsdOnce() throws Exception {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+        verify(mStatsManager, times(1))
+                .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
+        assertThat(mPublisher.hasDataSubscriber(DATA_SUBSCRIBER_1)).isTrue();
+    }
+
+    @Test
+    public void testAddDataSubscriber_whenRestarted_addsToStatsdOnce() throws Exception {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+        StatsPublisher publisher2 = createRestartedPublisher();
+
+        publisher2.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+        verify(mStatsManager, times(1))
+                .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
+        assertThat(publisher2.hasDataSubscriber(DATA_SUBSCRIBER_1)).isTrue();
+    }
+
+    @Test
+    public void testAddDataSubscriber_forProcessMemoryState_generatesStatsdMetrics()
+            throws Exception {
+        DataSubscriber processMemoryStateSubscriber =
+                new DataSubscriber(null, METRICS_CONFIG, SUBSCRIBER_2);
+
+        mPublisher.addDataSubscriber(processMemoryStateSubscriber);
+
+        verify(mStatsManager, times(1))
+                .addConfig(SUBSCRIBER_2_HASH, STATSD_CONFIG_2.toByteArray());
+        assertThat(mPublisher.hasDataSubscriber(processMemoryStateSubscriber)).isTrue();
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_removesFromStatsd() throws Exception {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+        mPublisher.removeDataSubscriber(DATA_SUBSCRIBER_1);
+
+        verify(mStatsManager, times(1)).removeConfig(SUBSCRIBER_1_HASH);
+        assertThat(getSavedStatsConfigs().keySet()).isEmpty();
+        assertThat(mPublisher.hasDataSubscriber(DATA_SUBSCRIBER_1)).isFalse();
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_ifNotFound_nothingHappensButCallsStatsdRemove()
+            throws Exception {
+        mPublisher.removeDataSubscriber(DATA_SUBSCRIBER_1);
+
+        // 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(DATA_SUBSCRIBER_1)).isFalse();
+    }
+
+    @Test
+    public void testRemoveAllDataSubscriber_whenRestarted_removesFromStatsdAndClears()
+            throws Exception {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+        StatsPublisher publisher2 = createRestartedPublisher();
+
+        publisher2.removeAllDataSubscribers();
+
+        verify(mStatsManager, times(1)).removeConfig(SUBSCRIBER_1_HASH);
+        assertThat(getSavedStatsConfigs().keySet()).isEmpty();
+        assertThat(publisher2.hasDataSubscriber(DATA_SUBSCRIBER_1)).isFalse();
+    }
+
+    @Test
+    public void testAddDataSubscriber_queuesPeriodicTaskInTheHandler() {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+        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(DATA_SUBSCRIBER_1);
+
+        assertThat(mPublisherFailure).hasMessageThat().contains("Failed to add config");
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_removesPeriodicStatsdReportPull() {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+        mPublisher.removeDataSubscriber(DATA_SUBSCRIBER_1);
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).isEmpty();
+    }
+
+    @Test
+    public void testRemoveAllDataSubscriber_removesPeriodicStatsdReportPull() {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+        mPublisher.removeAllDataSubscribers();
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).isEmpty();
+    }
+
+    @Test
+    public void testAfterDispatchItSchedulesANewPullReportTask() throws Exception {
+        mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+        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 {
+        DataSubscriber subscriber = Mockito.mock(DataSubscriber.class);
+        when(subscriber.getSubscriber()).thenReturn(SUBSCRIBER_1);
+        when(subscriber.getMetricsConfig()).thenReturn(METRICS_CONFIG);
+        when(subscriber.getPublisherParam()).thenReturn(SUBSCRIBER_1.getPublisher());
+        mPublisher.addDataSubscriber(subscriber);
+        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(subscriber).push(mBundleCaptor.capture());
+        assertThat(mBundleCaptor.getValue().getInt("reportsCount")).isEqualTo(2);
+    }
+
+    private PersistableBundle getSavedStatsConfigs() throws Exception {
+        File savedConfigsFile = new File(mRootDirectory, StatsPublisher.SAVED_STATS_CONFIGS_FILE);
+        try (FileInputStream fileInputStream = new FileInputStream(savedConfigsFile)) {
+            return PersistableBundle.readFromStream(fileInputStream);
+        }
+    }
+
+    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 bcd408a..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
@@ -27,6 +27,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -34,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;
@@ -74,7 +77,7 @@
                             .setVehiclePropertyId(-200))
                     .build();
 
-    // mMockCarPropertyService supported car property list.
+    // CarPropertyConfigs for mMockCarPropertyService.
     private static final CarPropertyConfig<Integer> PROP_CONFIG_1 =
             CarPropertyConfig.newBuilder(Integer.class, PROP_ID_1, AREA_ID).setAccess(
                     CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ).build();
@@ -82,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;
 
@@ -100,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
@@ -108,7 +116,21 @@
         mVehiclePropertyPublisher.addDataSubscriber(mMockDataSubscriber);
 
         verify(mMockCarPropertyService).registerListener(eq(PROP_ID_1), eq(PROP_READ_RATE), any());
-        assertThat(mVehiclePropertyPublisher.getDataSubscribers()).hasSize(1);
+        assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+    }
+
+    @Test
+    public void testAddDataSubscriber_withSamePropertyId_registersSingleListener() {
+        DataSubscriber subscriber2 = mock(DataSubscriber.class);
+        when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+
+        mVehiclePropertyPublisher.addDataSubscriber(mMockDataSubscriber);
+        mVehiclePropertyPublisher.addDataSubscriber(subscriber2);
+
+        verify(mMockCarPropertyService, times(1))
+                .registerListener(eq(PROP_ID_1), eq(PROP_READ_RATE), any());
+        assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+        assertThat(mVehiclePropertyPublisher.hasDataSubscriber(subscriber2)).isTrue();
     }
 
     @Test
@@ -125,7 +147,7 @@
                 () -> mVehiclePropertyPublisher.addDataSubscriber(invalidDataSubscriber));
 
         assertThat(error).hasMessageThat().contains("No access.");
-        assertThat(mVehiclePropertyPublisher.getDataSubscribers()).isEmpty();
+        assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
     }
 
     @Test
@@ -137,7 +159,7 @@
                 () -> mVehiclePropertyPublisher.addDataSubscriber(invalidDataSubscriber));
 
         assertThat(error).hasMessageThat().contains("not found");
-        assertThat(mVehiclePropertyPublisher.getDataSubscribers()).isEmpty();
+        assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
     }
 
     @Test
@@ -145,21 +167,28 @@
         mVehiclePropertyPublisher.addDataSubscriber(mMockDataSubscriber);
 
         mVehiclePropertyPublisher.removeDataSubscriber(mMockDataSubscriber);
-        // TODO(b/189143814): add proper verification
+
+        verify(mMockCarPropertyService, times(1)).unregisterListener(eq(PROP_ID_1), any());
+        assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
     }
 
     @Test
-    public void testRemoveDataSubscriber_failsIfNotFound() {
-        Throwable error = assertThrows(IllegalArgumentException.class,
-                () -> mVehiclePropertyPublisher.removeDataSubscriber(mMockDataSubscriber));
-
-        assertThat(error).hasMessageThat().contains("subscriber not found");
+    public void testRemoveDataSubscriber_ignoresIfNotFound() {
+        mVehiclePropertyPublisher.removeDataSubscriber(mMockDataSubscriber);
     }
 
     @Test
     public void testRemoveAllDataSubscribers_succeeds() {
+        DataSubscriber subscriber2 = mock(DataSubscriber.class);
+        when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+        mVehiclePropertyPublisher.addDataSubscriber(mMockDataSubscriber);
+        mVehiclePropertyPublisher.addDataSubscriber(subscriber2);
+
         mVehiclePropertyPublisher.removeAllDataSubscribers();
-        // TODO(b/189143814): add tests
+
+        assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+        assertThat(mVehiclePropertyPublisher.hasDataSubscriber(subscriber2)).isFalse();
+        verify(mMockCarPropertyService, times(1)).unregisterListener(eq(PROP_ID_1), any());
     }
 
     @Test
@@ -171,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/SystemMonitorTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorTest.java
new file mode 100644
index 0000000..bb61bf8
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.systemmonitor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+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;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.os.Handler;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SystemMonitorTest {
+
+    @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    private static final String TEST_LOADAVG = "1.2 3.4 2.2 123/1452 21348";
+    private static final String TEST_LOADAVG_BAD_FORMAT = "1.2 3.4";
+    private static final String TEST_LOADAVG_NOT_FLOAT = "1.2 abc 2.1 12/231 2";
+    private static final long TEST_AVAILMEM = 3_000_000_000L;
+    private static final long TEST_TOTALMEM = 8_000_000_000L;
+
+    @Mock private Context mMockContext;
+    @Mock private Handler mMockHandler; // it promptly executes the runnable in the same thread
+    @Mock private ActivityManager mMockActivityManager;
+    @Mock private SystemMonitor.SystemMonitorCallback mMockCallback;
+
+    @Captor ArgumentCaptor<Runnable> mRunnableCaptor;
+    @Captor ArgumentCaptor<SystemMonitorEvent> mEventCaptor;
+
+    @Before
+    public void setup() {
+        when(mMockContext.getSystemService(anyString())).thenReturn(mMockActivityManager);
+        when(mMockHandler.post(any(Runnable.class))).thenAnswer(i -> {
+            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
+    public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForHighUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 1.5);
+
+        assertThat(event.getCpuUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_HI);
+    }
+
+    @Test
+    public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForMedUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 0.6);
+
+        assertThat(event.getCpuUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_MED);
+    }
+
+    @Test
+    public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForLowUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 0.5);
+
+        assertThat(event.getCpuUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_LOW);
+    }
+
+    @Test
+    public void testSetEventMemUsageLevel_setsCorrectUsageLevelForHighUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.98);
+
+        assertThat(event.getMemoryUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_HI);
+    }
+
+    @Test
+    public void testSetEventMemUsageLevel_setsCorrectUsageLevelForMedUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.85);
+
+        assertThat(event.getMemoryUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_MED);
+    }
+
+    @Test
+    public void testSetEventMemUsageLevel_setsCorrectUsageLevelForLowUsage() {
+        SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+        SystemMonitorEvent event = new SystemMonitorEvent();
+
+        systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.80);
+
+        assertThat(event.getMemoryUsageLevel())
+            .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_LOW);
+    }
+
+    @Test
+    public void testAfterSetCallback_callbackCalled() throws IOException {
+        SystemMonitor systemMonitor = new SystemMonitor(
+                mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG));
+
+        systemMonitor.setSystemMonitorCallback(mMockCallback);
+
+        verify(mMockCallback, atLeastOnce()).onSystemMonitorEvent(mEventCaptor.capture());
+        SystemMonitorEvent event = mEventCaptor.getValue();
+        assertThat(event.getCpuUsageLevel()).isAnyOf(
+                SystemMonitorEvent.USAGE_LEVEL_LOW,
+                SystemMonitorEvent.USAGE_LEVEL_MED,
+                SystemMonitorEvent.USAGE_LEVEL_HI);
+        assertThat(event.getMemoryUsageLevel()).isAnyOf(
+                SystemMonitorEvent.USAGE_LEVEL_LOW,
+                SystemMonitorEvent.USAGE_LEVEL_MED,
+                SystemMonitorEvent.USAGE_LEVEL_HI);
+    }
+
+    @Test
+    public void testWhenLoadavgIsBadFormat_getCpuLoadReturnsNull() throws IOException {
+        SystemMonitor systemMonitor = new SystemMonitor(
+                mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG_BAD_FORMAT));
+
+        assertThat(systemMonitor.getCpuLoad()).isNull();
+    }
+
+    @Test
+    public void testWhenLoadavgIsNotFloatParsable_getCpuLoadReturnsNull() throws IOException {
+        SystemMonitor systemMonitor = new SystemMonitor(
+                mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG_NOT_FLOAT));
+
+        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.
+     */
+    private String writeTempFile(String content) throws IOException {
+        File tempFile = temporaryFolder.newFile();
+        try (FileWriter fw = new FileWriter(tempFile)) {
+            fw.write(content);
+        }
+        return tempFile.getAbsolutePath();
+    }
+}
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 b12aa27..c1e3938 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,16 +21,18 @@
 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;
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyList;
@@ -61,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;
@@ -80,7 +84,9 @@
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
@@ -90,12 +96,13 @@
 import android.util.ArraySet;
 import android.util.SparseArray;
 
-import com.android.car.CarLog;
+import com.android.car.CarLocalServices;
 import com.android.car.CarServiceUtils;
-import com.android.internal.util.function.TriConsumer;
+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;
@@ -104,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;
@@ -112,55 +122,99 @@
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
 
 /**
  * <p>This class contains unit tests for the {@link CarWatchdogService}.
  */
 @RunWith(MockitoJUnitRunner.class)
-public class CarWatchdogServiceUnitTest extends AbstractExtendedMockitoTestCase {
-    static final String TAG = CarLog.tagFor(CarWatchdogService.class);
+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 final SparseArray<String> mPackageNamesByUids = new SparseArray<>();
-    private final SparseArray<String[]> mSharedPackagesByUids = new SparseArray<>();
-    private final ArrayMap<String, ApplicationInfo> mApplicationInfosByPackages = new ArrayMap<>();
+    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();
-        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
@@ -215,6 +269,7 @@
         verify(mMockCarWatchdogDaemon)
                 .notifySystemStateChange(
                         eq(StateType.GARAGE_MODE), eq(GarageMode.GARAGE_MODE_ON), eq(-1));
+        verify(mMockWatchdogStorage).shrinkDatabase();
     }
 
     @Test
@@ -224,86 +279,320 @@
         verify(mMockCarWatchdogDaemon)
                 .notifySystemStateChange(
                         eq(StateType.GARAGE_MODE), eq(GarageMode.GARAGE_MODE_OFF), eq(-1));
+        verify(mMockWatchdogStorage, never()).shrinkDatabase();
     }
 
     @Test
     public void testGetResourceOveruseStats() throws Exception {
-        mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
+        int uid = Binder.getCallingUid();
+        injectPackageInfos(Collections.singletonList(
+                constructPackageManagerPackageInfo(
+                        mMockContext.getPackageName(), uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
 
-        List<PackageIoOveruseStats> packageIoOveruseStats = Collections.singletonList(
-                constructPackageIoOveruseStats(
-                        Binder.getCallingUid(), /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)));
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+                injectIoOveruseStatsForPackages(
+                        mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                        /* shouldNotifyPackages= */ new ArraySet<>());
 
         ResourceOveruseStats expectedStats =
-                constructResourceOveruseStats(mPackageNamesByUids.keyAt(0),
-                        mPackageNamesByUids.valueAt(0),
-                        packageIoOveruseStats.get(0).ioOveruseStats);
+                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
+    public void testGetResourceOveruseStatsForSharedUid() throws Exception {
+        int sharedUid = Binder.getCallingUid();
+        injectPackageInfos(Collections.singletonList(
+                constructPackageManagerPackageInfo(
+                        mMockContext.getPackageName(), sharedUid, "system_shared_package",
+                        ApplicationInfo.FLAG_SYSTEM, 0)));
+
+        SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+                injectIoOveruseStatsForPackages(
+                        mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                        /* shouldNotifyPackages= */ new ArraySet<>());
+
+        ResourceOveruseStats expectedStats =
+                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);
+
+        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
     public void testGetAllResourceOveruseStatsWithNoMinimum() throws Exception {
-        mPackageNamesByUids.put(1103456, "third_party_package");
-        mPackageNamesByUids.put(1201278, "vendor_package.critical");
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
 
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
-                constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(0),
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
-                constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(1),
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
-                                /* writtenBytes= */constructPerStateBytes(5000, 6000, 9000),
-                                /* totalOveruses= */2)));
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+                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= */ 2)));
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         List<ResourceOveruseStats> expectedStats = Arrays.asList(
-                constructResourceOveruseStats(mPackageNamesByUids.keyAt(0),
-                        mPackageNamesByUids.valueAt(0),
+                constructResourceOveruseStats(1103456, "third_party_package", 1,
                         packageIoOveruseStats.get(0).ioOveruseStats),
-                constructResourceOveruseStats(mPackageNamesByUids.keyAt(1),
-                        mPackageNamesByUids.valueAt(1),
+                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
+    public void testGetAllResourceOveruseStatsForSharedPackage() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "vendor_package.A", 1103456, "vendor_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.B", 1103456, "vendor_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "system_package.C", 1201000, "system_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "system_package.D", 1201000, "system_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.A", 1303456, "vendor_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.B", 1303456, "vendor_shared_package")));
+
+        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(1201000,
+                        /* shouldNotify= */ false,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+                                /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+                                /* writtenBytes= */ constructPerStateBytes(5000, 6000, 9000),
+                                /* totalOveruses= */ 0)),
+                constructPackageIoOveruseStats(1303456,
+                        /* shouldNotify= */ true,
+                        constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+                                /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* writtenBytes= */ constructPerStateBytes(80, 170, 260),
+                                /* totalOveruses= */ 1)));
+
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+        List<ResourceOveruseStats> expectedStats = Arrays.asList(
+                constructResourceOveruseStats(1103456, "shared:vendor_shared_package", 0,
+                        packageIoOveruseStats.get(0).ioOveruseStats),
+                constructResourceOveruseStats(1201278, "shared:system_shared_package", 0,
+                        packageIoOveruseStats.get(1).ioOveruseStats),
+                constructResourceOveruseStats(1303456, "shared:vendor_shared_package", 1,
+                        packageIoOveruseStats.get(2).ioOveruseStats));
+
+        List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
+                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,
@@ -315,38 +604,36 @@
 
         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
     public void testGetAllResourceOveruseStatsWithMinimum() throws Exception {
-        mPackageNamesByUids.put(1103456, "third_party_package");
-        mPackageNamesByUids.put(1201278, "vendor_package.critical");
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
 
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
-                constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(0),
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
-                constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(1),
-                        /* 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(mPackageNamesByUids.keyAt(1),
-                        mPackageNamesByUids.valueAt(1),
+                constructResourceOveruseStats(1201278, "vendor_package.critical", 0,
                         packageIoOveruseStats.get(1).ioOveruseStats));
 
         List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
@@ -356,31 +643,89 @@
 
         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
     public void testGetResourceOveruseStatsForUserPackage() throws Exception {
-        mPackageNamesByUids.put(1103456, "third_party_package");
-        mPackageNamesByUids.put(1201278, "vendor_package.critical");
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
 
         List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
-                constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(0),
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */true,
-                                /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                                /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                                /* totalOveruses= */2)),
-                constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(1),
-                        /* shouldNotify= */false,
-                        constructInternalIoOveruseStats(/* killableOnOveruse= */false,
-                                /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
-                                /* writtenBytes= */constructPerStateBytes(500, 600, 900),
-                                /* 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(500, 600, 900),
+                                /* totalOveruses= */ 2)));
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         ResourceOveruseStats expectedStats =
-                constructResourceOveruseStats(mPackageNamesByUids.keyAt(1),
-                        mPackageNamesByUids.valueAt(1),
+                constructResourceOveruseStats(1201278, "vendor_package.critical", 0,
                         packageIoOveruseStats.get(1).ioOveruseStats);
 
         ResourceOveruseStats actualStats =
@@ -389,22 +734,98 @@
                         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(
+                constructPackageManagerPackageInfo(
+                        "third_party_package", 1103456, "vendor_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package", 1103456, "vendor_shared_package"),
+                constructPackageManagerPackageInfo("system_package", 1101100,
+                        "shared_system_package")));
+
+        SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+                injectIoOveruseStatsForPackages(
+                        mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(
+                                Collections.singleton("shared:vendor_shared_package")),
+                        /* shouldNotifyPackages= */ new ArraySet<>());
+
+        ResourceOveruseStats expectedStats =
+                constructResourceOveruseStats(1103456, "shared:vendor_shared_package", 0,
+                        packageIoOveruseStatsByUid.get(1103456).ioOveruseStats);
+
+        ResourceOveruseStats actualStats =
+                mCarWatchdogService.getResourceOveruseStatsForUserPackage(
+                        "vendor_package", new UserHandle(11),
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                        CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+        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,
@@ -414,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
@@ -433,7 +854,7 @@
 
     @Test
     public void testResourceOveruseListener() throws Exception {
-        mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
+        mGenericPackageNameByUid.put(Binder.getCallingUid(), mMockContext.getPackageName());
 
         IResourceOveruseListener mockListener = createMockResourceOveruseListener();
         IBinder mockBinder = mockListener.asBinder();
@@ -443,9 +864,9 @@
 
         verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
-        injectIoOveruseStatsForPackages(mPackageNamesByUids,
-                /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+        injectIoOveruseStatsForPackages(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singleton(mMockContext.getPackageName())));
 
         verify(mockListener).onOveruse(any());
@@ -455,9 +876,9 @@
         verify(mockListener, atLeastOnce()).asBinder();
         verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
-        injectIoOveruseStatsForPackages(mPackageNamesByUids,
-                /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+        injectIoOveruseStatsForPackages(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singletonList(mMockContext.getPackageName())));
 
         verifyNoMoreInteractions(mockListener);
@@ -465,7 +886,7 @@
 
     @Test
     public void testDuplicateAddResourceOveruseListener() throws Exception {
-        mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
+        mGenericPackageNameByUid.put(Binder.getCallingUid(), mMockContext.getPackageName());
 
         IResourceOveruseListener mockListener = createMockResourceOveruseListener();
         IBinder mockBinder = mockListener.asBinder();
@@ -489,7 +910,7 @@
 
     @Test
     public void testAddMultipleResourceOveruseListeners() throws Exception {
-        mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
+        mGenericPackageNameByUid.put(Binder.getCallingUid(), mMockContext.getPackageName());
 
         IResourceOveruseListener firstMockListener = createMockResourceOveruseListener();
         IBinder firstMockBinder = firstMockListener.asBinder();
@@ -504,9 +925,9 @@
         verify(firstMockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
         verify(secondMockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
-        injectIoOveruseStatsForPackages(mPackageNamesByUids,
-                /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+        injectIoOveruseStatsForPackages(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singleton(mMockContext.getPackageName())));
 
         verify(firstMockListener).onOveruse(any());
@@ -516,9 +937,9 @@
         verify(firstMockListener, atLeastOnce()).asBinder();
         verify(firstMockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
-        injectIoOveruseStatsForPackages(mPackageNamesByUids,
-                /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+        injectIoOveruseStatsForPackages(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singletonList(mMockContext.getPackageName())));
 
         verify(secondMockListener, times(2)).onOveruse(any());
@@ -528,9 +949,9 @@
         verify(secondMockListener, atLeastOnce()).asBinder();
         verify(secondMockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
-        injectIoOveruseStatsForPackages(mPackageNamesByUids,
-                /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>(
+        injectIoOveruseStatsForPackages(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>(
                         Collections.singletonList(mMockContext.getPackageName())));
 
         verifyNoMoreInteractions(firstMockListener);
@@ -548,7 +969,7 @@
     @Test
     public void testResourceOveruseListenerForSystem() throws Exception {
         int callingUid = Binder.getCallingUid();
-        mPackageNamesByUids.put(callingUid, "critical.system.package");
+        mGenericPackageNameByUid.put(callingUid, "system_package.critical");
 
         IResourceOveruseListener mockListener = createMockResourceOveruseListener();
         mCarWatchdogService.addResourceOveruseListenerForSystem(
@@ -558,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());
 
@@ -573,118 +994,136 @@
         verify(mockListener, atLeastOnce()).asBinder();
         verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
 
-        mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
 
         verifyNoMoreInteractions(mockListener);
     }
 
     @Test
-    public void testSetKillablePackageAsUserWithPackageStats() throws Exception {
+    public void testSetKillablePackageAsUser() throws Exception {
         mockUmGetAliveUsers(mMockUserManager, 11, 12);
-        injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
-
-        mPackageNamesByUids.put(1103456, "third_party_package");
-        mPackageNamesByUids.put(1101278, "vendor_package.critical");
-        injectIoOveruseStatsForPackages(mPackageNamesByUids,
-                /* killablePackages= */new ArraySet<>(
-                        Collections.singletonList("third_party_package")),
-                /* shouldNotifyPackages= */new ArraySet<>());
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
+                constructPackageManagerPackageInfo("third_party_package", 1203456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
 
         UserHandle userHandle = new UserHandle(11);
-
         mCarWatchdogService.setKillablePackageAsUser("third_party_package", userHandle,
-                /* isKillable= */ true);
+                /* isKillable= */ false);
+        mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
+                userHandle, /* isKillable= */ false);
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+                new PackageKillableState("third_party_package", 11,
+                        PackageKillableState.KILLABLE_STATE_NO),
+                new PackageKillableState("vendor_package.critical", 11,
+                        PackageKillableState.KILLABLE_STATE_NEVER),
+                new PackageKillableState("third_party_package", 12,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("vendor_package.critical", 12,
+                        PackageKillableState.KILLABLE_STATE_NEVER));
+
         assertThrows(IllegalArgumentException.class,
                 () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
                         userHandle, /* isKillable= */ true));
 
-        PackageKillableStateSubject.assertThat(
-                mCarWatchdogService.getPackageKillableStatesAsUser(userHandle)).containsExactly(
-                new PackageKillableState("third_party_package", 11,
-                        PackageKillableState.KILLABLE_STATE_YES),
-                new PackageKillableState("vendor_package.critical", 11,
-                        PackageKillableState.KILLABLE_STATE_NEVER));
-
-        mCarWatchdogService.setKillablePackageAsUser("third_party_package", userHandle,
-                /* isKillable= */ false);
-        assertThrows(IllegalArgumentException.class,
-                () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
-                        userHandle, /* isKillable= */ false));
+        mockUmGetAliveUsers(mMockUserManager, 11, 12, 13);
+        injectPackageInfos(Collections.singletonList(
+                constructPackageManagerPackageInfo("third_party_package", 1303456, null)));
 
         PackageKillableStateSubject.assertThat(
-                mCarWatchdogService.getPackageKillableStatesAsUser(userHandle)).containsExactly(
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
                 new PackageKillableState("third_party_package", 11,
                         PackageKillableState.KILLABLE_STATE_NO),
                 new PackageKillableState("vendor_package.critical", 11,
-                        PackageKillableState.KILLABLE_STATE_NEVER));
+                        PackageKillableState.KILLABLE_STATE_NEVER),
+                new PackageKillableState("third_party_package", 12,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("vendor_package.critical", 12,
+                        PackageKillableState.KILLABLE_STATE_NEVER),
+                new PackageKillableState("third_party_package", 13,
+                        PackageKillableState.KILLABLE_STATE_YES));
     }
 
     @Test
-    public void testSetKillablePackageAsUserWithNoPackageStats() throws Exception {
+    public void testSetKillablePackageAsUserWithSharedUids() throws Exception {
         mockUmGetAliveUsers(mMockUserManager, 11, 12);
-        injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "third_party_package.A", 1103456, "third_party_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.B", 1103456, "third_party_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.C", 1101356, "third_party_shared_package.B"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.D", 1101356, "third_party_shared_package.B")));
 
         UserHandle userHandle = new UserHandle(11);
-        mCarWatchdogService.setKillablePackageAsUser("third_party_package", userHandle,
-                /* isKillable= */ true);
-        mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
-                userHandle, /* isKillable= */ true);
-
-        PackageKillableStateSubject.assertThat(
-                mCarWatchdogService.getPackageKillableStatesAsUser(userHandle)).containsExactly(
-                new PackageKillableState("third_party_package", 11,
-                        PackageKillableState.KILLABLE_STATE_YES),
-                new PackageKillableState("vendor_package.critical", 11,
-                        PackageKillableState.KILLABLE_STATE_NEVER));
-
-        mCarWatchdogService.setKillablePackageAsUser("third_party_package", userHandle,
+        mCarWatchdogService.setKillablePackageAsUser("third_party_package.A", userHandle,
                 /* isKillable= */ false);
-        assertThrows(IllegalArgumentException.class,
-                () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
-                        userHandle, /* isKillable= */ false));
 
         PackageKillableStateSubject.assertThat(
-                mCarWatchdogService.getPackageKillableStatesAsUser(userHandle)).containsExactly(
-                new PackageKillableState("third_party_package", 11,
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+                new PackageKillableState("third_party_package.A", 11,
                         PackageKillableState.KILLABLE_STATE_NO),
-                new PackageKillableState("vendor_package.critical", 11,
-                        PackageKillableState.KILLABLE_STATE_NEVER));
+                new PackageKillableState("third_party_package.B", 11,
+                        PackageKillableState.KILLABLE_STATE_NO),
+                new PackageKillableState("third_party_package.C", 11,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("third_party_package.D", 11,
+                        PackageKillableState.KILLABLE_STATE_YES));
+
+        mCarWatchdogService.setKillablePackageAsUser("third_party_package.B", userHandle,
+                /* isKillable= */ true);
+        mCarWatchdogService.setKillablePackageAsUser("third_party_package.C", userHandle,
+                /* isKillable= */ false);
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+                new PackageKillableState("third_party_package.A", 11,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("third_party_package.B", 11,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("third_party_package.C", 11,
+                        PackageKillableState.KILLABLE_STATE_NO),
+                new PackageKillableState("third_party_package.D", 11,
+                        PackageKillableState.KILLABLE_STATE_NO));
     }
 
     @Test
-    public void testSetKillablePackageAsUserForAllUsersWithPackageStats() throws Exception {
+    public void testSetKillablePackageAsUserForAllUsers() throws Exception {
         mockUmGetAliveUsers(mMockUserManager, 11, 12);
-        injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
-
-        mPackageNamesByUids.put(1103456, "third_party_package");
-        mPackageNamesByUids.put(1101278, "vendor_package.critical");
-        injectIoOveruseStatsForPackages(mPackageNamesByUids,
-                /* killablePackages= */new ArraySet<>(
-                        Collections.singletonList("third_party_package")),
-                /* shouldNotifyPackages= */new ArraySet<>());
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
+                constructPackageManagerPackageInfo("third_party_package", 1203456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
 
         mCarWatchdogService.setKillablePackageAsUser("third_party_package", UserHandle.ALL,
-                /* isKillable= */ true);
+                /* isKillable= */ false);
+        mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
+                UserHandle.ALL, /* isKillable= */ false);
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+                new PackageKillableState("third_party_package", 11,
+                        PackageKillableState.KILLABLE_STATE_NO),
+                new PackageKillableState("vendor_package.critical", 11,
+                        PackageKillableState.KILLABLE_STATE_NEVER),
+                new PackageKillableState("third_party_package", 12,
+                        PackageKillableState.KILLABLE_STATE_NO),
+                new PackageKillableState("vendor_package.critical", 12,
+                        PackageKillableState.KILLABLE_STATE_NEVER));
+
         assertThrows(IllegalArgumentException.class,
                 () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
                         UserHandle.ALL, /* isKillable= */ true));
 
-        PackageKillableStateSubject.assertThat(
-                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
-                new PackageKillableState("third_party_package", 11,
-                        PackageKillableState.KILLABLE_STATE_YES),
-                new PackageKillableState("vendor_package.critical", 11,
-                        PackageKillableState.KILLABLE_STATE_NEVER),
-                new PackageKillableState("third_party_package", 12,
-                        PackageKillableState.KILLABLE_STATE_YES),
-                new PackageKillableState("vendor_package.critical", 12,
-                        PackageKillableState.KILLABLE_STATE_NEVER));
-
-        mCarWatchdogService.setKillablePackageAsUser("third_party_package", UserHandle.ALL,
-                /* isKillable= */ false);
-        assertThrows(IllegalArgumentException.class,
-                () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
-                        UserHandle.ALL, /* isKillable= */ false));
+        mockUmGetAliveUsers(mMockUserManager, 11, 12, 13);
+        injectPackageInfos(Collections.singletonList(
+                constructPackageManagerPackageInfo("third_party_package", 1303456, null)));
 
         PackageKillableStateSubject.assertThat(
                 mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
@@ -695,52 +1134,71 @@
                 new PackageKillableState("third_party_package", 12,
                         PackageKillableState.KILLABLE_STATE_NO),
                 new PackageKillableState("vendor_package.critical", 12,
-                        PackageKillableState.KILLABLE_STATE_NEVER));
+                        PackageKillableState.KILLABLE_STATE_NEVER),
+                new PackageKillableState("third_party_package", 13,
+                        PackageKillableState.KILLABLE_STATE_NO));
     }
 
     @Test
-    public void testSetKillablePackageAsUserForAllUsersWithNoPackageStats() throws Exception {
+    public void testSetKillablePackageAsUsersForAllUsersWithSharedUids() throws Exception {
         mockUmGetAliveUsers(mMockUserManager, 11, 12);
-        injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "third_party_package.A", 1103456, "third_party_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.B", 1103456, "third_party_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.C", 1101356, "third_party_shared_package.B"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.D", 1101356, "third_party_shared_package.B"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.A", 1203456, "third_party_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.B", 1203456, "third_party_shared_package.A")));
 
-        mCarWatchdogService.setKillablePackageAsUser("third_party_package", UserHandle.ALL,
-                /* isKillable= */ true);
-        mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
-                UserHandle.ALL, /* isKillable= */ true);
-
-        PackageKillableStateSubject.assertThat(
-                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
-                new PackageKillableState("third_party_package", 11,
-                        PackageKillableState.KILLABLE_STATE_YES),
-                new PackageKillableState("vendor_package.critical", 11,
-                        PackageKillableState.KILLABLE_STATE_NEVER),
-                new PackageKillableState("third_party_package", 12,
-                        PackageKillableState.KILLABLE_STATE_YES),
-                new PackageKillableState("vendor_package.critical", 12,
-                        PackageKillableState.KILLABLE_STATE_NEVER));
-
-        mCarWatchdogService.setKillablePackageAsUser("third_party_package", UserHandle.ALL,
+        mCarWatchdogService.setKillablePackageAsUser("third_party_package.A", UserHandle.ALL,
                 /* isKillable= */ false);
-        assertThrows(IllegalArgumentException.class,
-                () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
-                        UserHandle.ALL, /* isKillable= */ false));
 
         PackageKillableStateSubject.assertThat(
                 mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
-                new PackageKillableState("third_party_package", 11,
+                new PackageKillableState("third_party_package.A", 11,
                         PackageKillableState.KILLABLE_STATE_NO),
-                new PackageKillableState("vendor_package.critical", 11,
-                        PackageKillableState.KILLABLE_STATE_NEVER),
-                new PackageKillableState("third_party_package", 12,
+                new PackageKillableState("third_party_package.B", 11,
                         PackageKillableState.KILLABLE_STATE_NO),
-                new PackageKillableState("vendor_package.critical", 12,
-                        PackageKillableState.KILLABLE_STATE_NEVER));
+                new PackageKillableState("third_party_package.C", 11,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("third_party_package.D", 11,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("third_party_package.A", 12,
+                        PackageKillableState.KILLABLE_STATE_NO),
+                new PackageKillableState("third_party_package.B", 12,
+                        PackageKillableState.KILLABLE_STATE_NO));
+
+        mockUmGetAliveUsers(mMockUserManager, 11, 12, 13);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "third_party_package.A", 1303456, "third_party_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.B", 1303456, "third_party_shared_package.A")));
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(13)))
+                .containsExactly(
+                new PackageKillableState("third_party_package.A", 13,
+                        PackageKillableState.KILLABLE_STATE_NO),
+                new PackageKillableState("third_party_package.B", 13,
+                        PackageKillableState.KILLABLE_STATE_NO));
     }
 
     @Test
     public void testGetPackageKillableStatesAsUser() throws Exception {
         mockUmGetAliveUsers(mMockUserManager, 11, 12);
-        injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
+                constructPackageManagerPackageInfo("third_party_package", 1203456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
         PackageKillableStateSubject.assertThat(
                 mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(11)))
                 .containsExactly(
@@ -751,9 +1209,178 @@
     }
 
     @Test
+    public void testGetPackageKillableStatesAsUserWithSafeToKillPackages() throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 11, 12);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("system_package.non_critical.A", 1102459, null),
+                constructPackageManagerPackageInfo("third_party_package", 1103456, 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.B", 1201278, null)));
+
+        List<android.automotive.watchdog.internal.ResourceOveruseConfiguration> configs =
+                sampleInternalResourceOveruseConfigurations();
+        injectResourceOveruseConfigsAndWait(configs);
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL))
+                .containsExactly(
+                        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.B", 11,
+                                PackageKillableState.KILLABLE_STATE_NEVER),
+                        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
+    public void testGetPackageKillableStatesAsUserWithVendorPackagePrefixes() throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 11);
+        /* Package names which start with "system" are constructed as system packages. */
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("system_package_as_vendor", 1102459, null)));
+
+        android.automotive.watchdog.internal.ResourceOveruseConfiguration vendorConfig =
+                new android.automotive.watchdog.internal.ResourceOveruseConfiguration();
+        vendorConfig.componentType = ComponentType.VENDOR;
+        vendorConfig.safeToKillPackages = Collections.singletonList("system_package_as_vendor");
+        vendorConfig.vendorPackagePrefixes = Collections.singletonList(
+                "system_package_as_vendor");
+        injectResourceOveruseConfigsAndWait(Collections.singletonList(vendorConfig));
+
+        List<PackageKillableState> killableStates =
+                mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(11));
+
+        /* When CarWatchdogService connects with the watchdog daemon, CarWatchdogService fetches
+         * resource overuse configs from watchdog daemon. The vendor package prefixes in the
+         * configs help identify vendor packages. The safe-to-kill list in the configs helps
+         * identify safe-to-kill vendor packages. |system_package_as_vendor| is a critical system
+         * package by default but with the latest resource overuse configs, this package should be
+         * classified as a safe-to-kill vendor package.
+         */
+        PackageKillableStateSubject.assertThat(killableStates)
+                .containsExactly(
+                        new PackageKillableState("system_package_as_vendor", 11,
+                                PackageKillableState.KILLABLE_STATE_YES));
+    }
+
+    @Test
+    public void testGetPackageKillableStatesAsUserWithSharedUids() throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 11, 12);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "system_package.A", 1103456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.B", 1103456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.C", 1105678, "third_party_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.D", 1105678, "third_party_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "system_package.A", 1203456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.B", 1203456, "vendor_shared_package.A")));
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(11)))
+                .containsExactly(
+                        new PackageKillableState("system_package.A", 11,
+                                PackageKillableState.KILLABLE_STATE_NEVER),
+                        new PackageKillableState("vendor_package.B", 11,
+                                PackageKillableState.KILLABLE_STATE_NEVER),
+                        new PackageKillableState("third_party_package.C", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("third_party_package.D", 11,
+                                PackageKillableState.KILLABLE_STATE_YES));
+    }
+
+    @Test
+    public void testGetPackageKillableStatesAsUserWithSharedUidsAndSafeToKillPackages()
+            throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 11);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "vendor_package.non_critical.A", 1103456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "system_package.A", 1103456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.B", 1103456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.C", 1105678, "third_party_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.D", 1105678, "third_party_shared_package")));
+
+        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");
+        vendorConfig.vendorPackagePrefixes = new ArrayList<>();
+        injectResourceOveruseConfigsAndWait(Collections.singletonList(vendorConfig));
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(11)))
+                .containsExactly(
+                        new PackageKillableState("vendor_package.non_critical.A", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("system_package.A", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("vendor_package.B", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("third_party_package.C", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("third_party_package.D", 11,
+                                PackageKillableState.KILLABLE_STATE_YES));
+    }
+
+    @Test
+    public void testGetPackageKillableStatesAsUserWithSharedUidsAndSafeToKillSharedPackage()
+            throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 11);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "vendor_package.non_critical.A", 1103456, "vendor_shared_package.B"),
+                constructPackageManagerPackageInfo(
+                        "system_package.non_critical.A", 1103456, "vendor_shared_package.B"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.non_critical.B", 1103456, "vendor_shared_package.B")));
+
+        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");
+        vendorConfig.vendorPackagePrefixes = new ArrayList<>();
+        injectResourceOveruseConfigsAndWait(Collections.singletonList(vendorConfig));
+
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(11)))
+                .containsExactly(
+                        new PackageKillableState("vendor_package.non_critical.A", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("system_package.non_critical.A", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("vendor_package.non_critical.B", 11,
+                                PackageKillableState.KILLABLE_STATE_YES));
+    }
+
+    @Test
     public void testGetPackageKillableStatesAsUserForAllUsers() throws Exception {
         mockUmGetAliveUsers(mMockUserManager, 11, 12);
-        injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
+                constructPackageManagerPackageInfo("third_party_package", 1203456, null),
+                constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
         PackageKillableStateSubject.assertThat(
                 mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
                 new PackageKillableState("third_party_package", 11,
@@ -767,11 +1394,48 @@
     }
 
     @Test
+    public void testGetPackageKillableStatesAsUserForAllUsersWithSharedUids() throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 11, 12);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "system_package.A", 1103456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.B", 1103456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.C", 1105678, "third_party_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.D", 1105678, "third_party_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "system_package.A", 1203456, "vendor_shared_package.A"),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.B", 1203456, "vendor_shared_package.A")));
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL))
+                .containsExactly(
+                        new PackageKillableState("system_package.A", 11,
+                                PackageKillableState.KILLABLE_STATE_NEVER),
+                        new PackageKillableState("vendor_package.B", 11,
+                                PackageKillableState.KILLABLE_STATE_NEVER),
+                        new PackageKillableState("third_party_package.C", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("third_party_package.D", 11,
+                                PackageKillableState.KILLABLE_STATE_YES),
+                        new PackageKillableState("system_package.A", 12,
+                                PackageKillableState.KILLABLE_STATE_NEVER),
+                        new PackageKillableState("vendor_package.B", 12,
+                                PackageKillableState.KILLABLE_STATE_NEVER));
+    }
+
+    @Test
     public void testSetResourceOveruseConfigurations() throws Exception {
         assertThat(mCarWatchdogService.setResourceOveruseConfigurations(
                 sampleResourceOveruseConfigurations(), CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO))
                 .isEqualTo(CarWatchdogManager.RETURN_CODE_SUCCESS);
 
+        /* Expect two calls, the first is made at car watchdog service init */
+        verifyResourceOveruseConfigurationsSynced(2);
+
         InternalResourceOveruseConfigurationSubject
                 .assertThat(captureOnSetResourceOveruseConfigurations())
                 .containsExactlyElementsIn(sampleInternalResourceOveruseConfigurations());
@@ -886,6 +1550,64 @@
     }
 
     @Test
+    public void testFailsSetResourceOveruseConfigurationsOnZeroComponentLevelIoOveruseThresholds()
+            throws Exception {
+        List<ResourceOveruseConfiguration> resourceOveruseConfigs =
+                Collections.singletonList(
+                        sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
+                                sampleIoOveruseConfigurationBuilder(ComponentType.SYSTEM)
+                                        .setComponentLevelThresholds(new PerStateBytes(200, 0, 200))
+                                        .build())
+                                .build());
+        assertThrows(IllegalArgumentException.class,
+                () -> mCarWatchdogService.setResourceOveruseConfigurations(resourceOveruseConfigs,
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
+    }
+
+    @Test
+    public void testFailsSetResourceOveruseConfigurationsOnEmptyIoOveruseSystemWideThresholds()
+            throws Exception {
+        List<ResourceOveruseConfiguration> resourceOveruseConfigs =
+                Collections.singletonList(
+                        sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
+                                sampleIoOveruseConfigurationBuilder(ComponentType.SYSTEM)
+                                        .setSystemWideThresholds(new ArrayList<>())
+                                        .build())
+                                .build());
+        assertThrows(IllegalArgumentException.class,
+                () -> mCarWatchdogService.setResourceOveruseConfigurations(resourceOveruseConfigs,
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
+    }
+
+    @Test
+    public void testFailsSetResourceOveruseConfigurationsOnIoOveruseInvalidSystemWideThreshold()
+            throws Exception {
+        List<ResourceOveruseConfiguration> resourceOveruseConfigs = new ArrayList<>();
+        resourceOveruseConfigs.add(sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
+                sampleIoOveruseConfigurationBuilder(ComponentType.SYSTEM)
+                        .setSystemWideThresholds(Collections.singletonList(
+                                new IoOveruseAlertThreshold(30, 0)))
+                        .build())
+                .build());
+        assertThrows(IllegalArgumentException.class,
+                () -> mCarWatchdogService.setResourceOveruseConfigurations(
+                        resourceOveruseConfigs,
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
+
+        resourceOveruseConfigs.set(0,
+                sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
+                        sampleIoOveruseConfigurationBuilder(ComponentType.SYSTEM)
+                                .setSystemWideThresholds(Collections.singletonList(
+                                        new IoOveruseAlertThreshold(0, 300)))
+                                .build())
+                        .build());
+        assertThrows(IllegalArgumentException.class,
+                () -> mCarWatchdogService.setResourceOveruseConfigurations(
+                        resourceOveruseConfigs,
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
+    }
+
+    @Test
     public void testFailsSetResourceOveruseConfigurationsOnNullIoOveruseConfiguration()
             throws Exception {
         List<ResourceOveruseConfiguration> resourceOveruseConfigs = Collections.singletonList(
@@ -916,7 +1638,8 @@
                 () -> mCarWatchdogService.getResourceOveruseConfigurations(
                         CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
 
-        verify(mMockCarWatchdogDaemon, never()).getResourceOveruseConfigurations();
+        /* Method initially called in CarWatchdogService init */
+        verify(mMockCarWatchdogDaemon).getResourceOveruseConfigurations();
     }
 
     @Test
@@ -991,14 +1714,19 @@
     @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;
 
-        mPackageNamesByUids.put(criticalSysPkgUid, "critical.system.package");
-        mPackageNamesByUids.put(nonCriticalSysPkgUid, "non_critical.system.package");
-        mPackageNamesByUids.put(nonCriticalVndrPkgUid, "non_critical.vendor.package");
-        mPackageNamesByUids.put(thirdPartyPkgUid, "third_party.package");
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "system_package.critical", criticalSysPkgUid, null),
+                constructPackageManagerPackageInfo(
+                        "system_package.non_critical", nonCriticalSysPkgUid, null),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.non_critical", nonCriticalVndrPkgUid, null),
+                constructPackageManagerPackageInfo(
+                        "third_party_package", thirdPartyPkgUid, null)));
 
         IResourceOveruseListener mockSystemListener = createMockResourceOveruseListener();
         mCarWatchdogService.addResourceOveruseListenerForSystem(
@@ -1008,67 +1736,149 @@
         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);
+
+        assertThat(mDisabledUserPackages).containsExactlyElementsIn(Collections.singleton(
+                "10:third_party_package"));
 
         List<ResourceOveruseStats> expectedStats = new ArrayList<>();
 
-        expectedStats.add(constructResourceOveruseStats(criticalSysPkgUid,
-                mPackageNamesByUids.get(criticalSysPkgUid),
+        expectedStats.add(constructResourceOveruseStats(
+                criticalSysPkgUid, "system_package.critical", 0,
                 packageIoOveruseStats.get(0).ioOveruseStats));
 
         verifyOnOveruseCalled(expectedStats, mockListener);
 
-        expectedStats.add(constructResourceOveruseStats(nonCriticalSysPkgUid,
-                mPackageNamesByUids.get(nonCriticalSysPkgUid),
+        expectedStats.add(constructResourceOveruseStats(
+                nonCriticalSysPkgUid, "system_package.non_critical", 0,
                 packageIoOveruseStats.get(1).ioOveruseStats));
 
-        expectedStats.add(constructResourceOveruseStats(thirdPartyPkgUid,
-                mPackageNamesByUids.get(thirdPartyPkgUid),
+        /*
+         * 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(
-                mPackageNamesByUids.get(thirdPartyPkgUid), UserHandle.getUserId(thirdPartyPkgUid));
-        verify(packageManagerService).setApplicationEnabledSetting(
-                eq(mPackageNamesByUids.get(thirdPartyPkgUid)), anyInt(), anyInt(),
-                eq(UserHandle.getUserId(thirdPartyPkgUid)), anyString());
+        List<PackageResourceOveruseAction> expectedActions = Arrays.asList(
+                constructPackageResourceOveruseAction(
+                        "system_package.critical",
+                        criticalSysPkgUid, new int[]{ResourceType.IO}, NOT_KILLED),
+                constructPackageResourceOveruseAction(
+                        "third_party_package",
+                        thirdPartyPkgUid, new int[]{ResourceType.IO}, KILLED));
+        verifyActionsTakenOnResourceOveruse(expectedActions);
+    }
+
+    @Test
+    public void testLatestIoOveruseStatsWithSharedUid() throws Exception {
+        int criticalSysSharedUid = Binder.getCallingUid();
+        int nonCriticalVndrSharedUid = 1002564;
+        int thirdPartySharedUid = 1002044;
+
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "system_package.A", criticalSysSharedUid, "system_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "system_package.B", criticalSysSharedUid, "system_shared_package"),
+                constructPackageManagerPackageInfo("vendor_package.non_critical",
+                        nonCriticalVndrSharedUid, "vendor_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.A", thirdPartySharedUid, "third_party_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.B", thirdPartySharedUid, "third_party_shared_package")
+        ));
+
+        IResourceOveruseListener mockSystemListener = createMockResourceOveruseListener();
+        mCarWatchdogService.addResourceOveruseListenerForSystem(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, mockSystemListener);
+
+        IResourceOveruseListener mockListener = createMockResourceOveruseListener();
+        mCarWatchdogService.addResourceOveruseListener(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, mockListener);
+
+        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)),
+                /* 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)),
+                /* 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)));
+
+        pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+        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", 0,
+                packageIoOveruseStats.get(0).ioOveruseStats));
+
+        verifyOnOveruseCalled(expectedStats, mockListener);
+
+        expectedStats.add(constructResourceOveruseStats(
+                nonCriticalVndrSharedUid, "shared:vendor_shared_package", 0,
+                packageIoOveruseStats.get(1).ioOveruseStats));
+
+        /*
+         * 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);
 
         List<PackageResourceOveruseAction> expectedActions = Arrays.asList(
-                constructPackageResourceOveruseAction(mPackageNamesByUids.get(criticalSysPkgUid),
-                        criticalSysPkgUid, new int[]{ResourceType.IO}, NOT_KILLED),
-                constructPackageResourceOveruseAction(mPackageNamesByUids.get(thirdPartyPkgUid),
-                        thirdPartyPkgUid, new int[]{ResourceType.IO}, KILLED));
+                constructPackageResourceOveruseAction(
+                        "shared:system_shared_package",
+                        criticalSysSharedUid, new int[]{ResourceType.IO}, NOT_KILLED),
+                constructPackageResourceOveruseAction(
+                        "shared:third_party_shared_package",
+                        thirdPartySharedUid, new int[]{ResourceType.IO}, KILLED));
         verifyActionsTakenOnResourceOveruse(expectedActions);
     }
 
@@ -1086,197 +1896,454 @@
     }
 
     @Test
-    public void testResetResourceOveruseStats() throws Exception {
-        mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
-        mPackageNamesByUids.put(1101278, "vendor_package.critical");
-        injectIoOveruseStatsForPackages(mPackageNamesByUids,
-                /* killablePackages= */new ArraySet<>(),
-                /* shouldNotifyPackages= */new ArraySet<>());
+    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)));
 
-        mWatchdogServiceForSystemImpl.resetResourceOveruseStats(
-                Collections.singletonList(mMockContext.getPackageName()));
+        SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+                injectIoOveruseStatsForPackages(
+                        mGenericPackageNameByUid,
+                        /* killablePackages= */ new ArraySet<>(Collections.singletonList(
+                                "third_party_package.A")),
+                        /* shouldNotifyPackages= */ new ArraySet<>());
 
-        ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
-                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+        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);
 
-        ResourceOveruseStats expectedStats = new ResourceOveruseStats.Builder(
-                mMockContext.getPackageName(),
-                UserHandle.getUserHandleForUid(Binder.getCallingUid())).build();
+        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));
 
-        assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
-                + actualStats.toString())
-                .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+        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 testResetResourceOveruseStatsResetsStats() throws Exception {
+        UserHandle user = UserHandle.getUserHandleForUid(10003346);
+        String packageName = mMockContext.getPackageName();
+        mGenericPackageNameByUid.put(10003346, packageName);
+        mGenericPackageNameByUid.put(10101278, "vendor_package.critical");
+        injectIoOveruseStatsForPackages(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>());
+
+        mWatchdogServiceForSystemImpl.resetResourceOveruseStats(
+                Collections.singletonList(packageName));
+
+        ResourceOveruseStats actualStats =
+                mCarWatchdogService.getResourceOveruseStatsForUserPackage(
+                        packageName, user,
+                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                        CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+        ResourceOveruseStats expectedStats = new ResourceOveruseStats.Builder(
+                packageName, user).build();
+
+        ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+
+        verify(mMockWatchdogStorage).deleteUserPackage(eq(user.getIdentifier()), eq(packageName));
+    }
+
+    @Test
+    public void testResetResourceOveruseStatsResetsUserPackageSettings() throws Exception {
+        mockUmGetAliveUsers(mMockUserManager, 100, 101);
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("third_party_package.A", 10001278, null),
+                constructPackageManagerPackageInfo("third_party_package.A", 10101278, null),
+                constructPackageManagerPackageInfo("third_party_package.B", 10003346, null),
+                constructPackageManagerPackageInfo("third_party_package.B", 10103346, null)));
+        injectIoOveruseStatsForPackages(mGenericPackageNameByUid,
+                /* killablePackages= */ Set.of("third_party_package.A", "third_party_package.B"),
+                /* shouldNotifyPackages= */ new ArraySet<>());
+
+        mCarWatchdogService.setKillablePackageAsUser("third_party_package.A",
+                UserHandle.ALL, /* isKillable= */false);
+        mCarWatchdogService.setKillablePackageAsUser("third_party_package.B",
+                UserHandle.ALL, /* isKillable= */false);
+
+        mWatchdogServiceForSystemImpl.resetResourceOveruseStats(
+                Collections.singletonList("third_party_package.A"));
+
+        PackageKillableStateSubject.assertThat(
+                mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+                new PackageKillableState("third_party_package.A", 100,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("third_party_package.A", 101,
+                        PackageKillableState.KILLABLE_STATE_YES),
+                new PackageKillableState("third_party_package.B", 100,
+                        PackageKillableState.KILLABLE_STATE_NO),
+                new PackageKillableState("third_party_package.B", 101,
+                        PackageKillableState.KILLABLE_STATE_NO)
+        );
+
+        verify(mMockWatchdogStorage, times(2)).deleteUserPackage(anyInt(),
+                eq("third_party_package.A"));
+    }
+
+    @Test
+    public void testSaveToStorageAfterResetResourceOveruseStats() throws Exception {
+        setDate(1);
+        mGenericPackageNameByUid.put(1011200, "system_package");
+        SparseArray<PackageIoOveruseStats> stats = injectIoOveruseStatsForPackages(
+                mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+                /* shouldNotifyPackages= */ new ArraySet<>());
+
+        mWatchdogServiceForSystemImpl.resetResourceOveruseStats(
+                Collections.singletonList("system_package"));
+
+        /* |resetResourceOveruseStats| sets the package's IoOveruseStats to null, packages with
+         * null I/O stats are not written to disk. Push new IoOveruseStats to the |system_package|
+         * so that the package can be written to the database when date changes.
+         */
+        pushLatestIoOveruseStatsAndWait(Collections.singletonList(stats.get(1011200)));
+
+        /* Force write to disk by changing the date and pushing new I/O overuse stats. */
+        setDate(0);
+        pushLatestIoOveruseStatsAndWait(Collections.singletonList(new PackageIoOveruseStats()));
+
+        WatchdogStorage.IoUsageStatsEntry expectedSavedEntries =
+                new WatchdogStorage.IoUsageStatsEntry(/* userId= */ 10, "system_package",
+                        new WatchdogPerfHandler.PackageIoUsage(stats.get(1011200).ioOveruseStats,
+                                /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+                                /* totalTimesKilled= */ 0));
+
+        IoUsageStatsEntrySubject.assertThat(mIoUsageStatsEntries)
+                .containsExactlyElementsIn(Collections.singletonList(expectedSavedEntries));
     }
 
     @Test
     public void testGetPackageInfosForUids() throws Exception {
-        int[] uids = new int[]{6001, 6050, 5100, 110035, 120056, 120078, 1345678};
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "system_package.A", 6001, null, ApplicationInfo.FLAG_SYSTEM, 0),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.B", 5100, null, 0, ApplicationInfo.PRIVATE_FLAG_OEM),
+                constructPackageManagerPackageInfo(
+                        "vendor_package.C", 1345678, null, 0, ApplicationInfo.PRIVATE_FLAG_ODM),
+                constructPackageManagerPackageInfo("third_party_package.D", 120056, null)));
+
+        int[] uids = new int[]{6001, 5100, 120056, 1345678};
+        List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
+                uids, new ArrayList<>());
+
         List<PackageInfo> expectedPackageInfos = Arrays.asList(
-                constructPackageInfo("system.package.A", 6001, new ArrayList<>(),
+                constructPackageInfo("system_package.A", 6001, new ArrayList<>(),
                         UidType.NATIVE, ComponentType.SYSTEM, ApplicationCategoryType.OTHERS),
-                constructPackageInfo("shared:system.package", 6050,
-                        Arrays.asList("system.package.B", "third_party.package.C"),
-                        UidType.NATIVE, ComponentType.SYSTEM, ApplicationCategoryType.OTHERS),
-                constructPackageInfo("vendor.package.D", 5100, new ArrayList<>(),
+                constructPackageInfo("vendor_package.B", 5100, new ArrayList<>(),
                         UidType.NATIVE, ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
-                constructPackageInfo("shared:vendor.package", 110035,
-                        Arrays.asList("vendor.package.E", "system.package.F",
-                                "third_party.package.G"), UidType.APPLICATION,
-                        ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
-                constructPackageInfo("third_party.package.H", 120056, new ArrayList<>(),
+                constructPackageInfo("third_party_package.D", 120056, new ArrayList<>(),
                         UidType.APPLICATION, ComponentType.THIRD_PARTY,
                         ApplicationCategoryType.OTHERS),
-                constructPackageInfo("shared:third_party.package", 120078,
-                        Collections.singletonList("third_party.package.I"),
-                        UidType.APPLICATION,  ComponentType.THIRD_PARTY,
-                        ApplicationCategoryType.OTHERS),
-                constructPackageInfo("vendor.package.J", 1345678, new ArrayList<>(),
+                constructPackageInfo("vendor_package.C", 1345678, new ArrayList<>(),
                         UidType.APPLICATION, ComponentType.VENDOR,
                         ApplicationCategoryType.OTHERS));
 
-        for (PackageInfo packageInfo : expectedPackageInfos) {
-            mPackageNamesByUids.put(packageInfo.packageIdentifier.uid,
-                    packageInfo.packageIdentifier.name);
-            mSharedPackagesByUids.put(packageInfo.packageIdentifier.uid,
-                    packageInfo.sharedUidPackages.toArray(new String[0]));
-        }
+        assertPackageInfoEquals(actualPackageInfos, expectedPackageInfos);
+    }
 
-        mApplicationInfosByPackages.put("system.package.A",
-                constructApplicationInfo(ApplicationInfo.FLAG_SYSTEM, 0));
-        mApplicationInfosByPackages.put("system.package.B",
-                constructApplicationInfo(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP, 0));
-        mApplicationInfosByPackages.put("system.package.F",
-                constructApplicationInfo(0, ApplicationInfo.PRIVATE_FLAG_PRODUCT));
-        mApplicationInfosByPackages.put("vendor.package.D",
-                constructApplicationInfo(0, ApplicationInfo.PRIVATE_FLAG_OEM));
-        mApplicationInfosByPackages.put("vendor.package.E",
-                constructApplicationInfo(0, ApplicationInfo.PRIVATE_FLAG_VENDOR));
-        mApplicationInfosByPackages.put("vendor.package.J",
-                constructApplicationInfo(0, ApplicationInfo.PRIVATE_FLAG_ODM));
-        mApplicationInfosByPackages.put("third_party.package.C", constructApplicationInfo(0, 0));
-        mApplicationInfosByPackages.put("third_party.package.G", constructApplicationInfo(0, 0));
-        mApplicationInfosByPackages.put("third_party.package.H", constructApplicationInfo(0, 0));
-        mApplicationInfosByPackages.put("third_party.package.I", constructApplicationInfo(0, 0));
+    @Test
+    public void testGetPackageInfosWithSharedUids() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo("system_package.A", 6050,
+                        "system_shared_package", ApplicationInfo.FLAG_UPDATED_SYSTEM_APP, 0),
+                constructPackageManagerPackageInfo("system_package.B", 110035,
+                        "vendor_shared_package", 0, ApplicationInfo.PRIVATE_FLAG_PRODUCT),
+                constructPackageManagerPackageInfo("vendor_package.C", 110035,
+                        "vendor_shared_package", 0, ApplicationInfo.PRIVATE_FLAG_VENDOR),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.D", 6050, "system_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.E", 110035, "vendor_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.F", 120078, "third_party_shared_package")));
 
+        int[] uids = new int[]{6050, 110035, 120056, 120078};
         List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
                 uids, new ArrayList<>());
 
+        List<PackageInfo> expectedPackageInfos = Arrays.asList(
+                constructPackageInfo("shared:system_shared_package", 6050,
+                        Arrays.asList("system_package.A", "third_party_package.D"),
+                        UidType.NATIVE, ComponentType.SYSTEM, ApplicationCategoryType.OTHERS),
+                constructPackageInfo("shared:vendor_shared_package", 110035,
+                        Arrays.asList("vendor_package.C", "system_package.B",
+                                "third_party_package.E"), UidType.APPLICATION,
+                        ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
+                constructPackageInfo("shared:third_party_shared_package", 120078,
+                        Collections.singletonList("third_party_package.F"),
+                        UidType.APPLICATION,  ComponentType.THIRD_PARTY,
+                        ApplicationCategoryType.OTHERS));
+
         assertPackageInfoEquals(actualPackageInfos, expectedPackageInfos);
     }
 
     @Test
     public void testGetPackageInfosForUidsWithVendorPackagePrefixes() throws Exception {
-        int[] uids = new int[]{110034, 110035, 123456, 120078};
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "vendor_package.A", 110034, null, 0, ApplicationInfo.PRIVATE_FLAG_PRODUCT),
+                constructPackageManagerPackageInfo("vendor_pkg.B", 110035,
+                        "vendor_shared_package", ApplicationInfo.FLAG_SYSTEM, 0),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.C", 110035, "vendor_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.D", 110035, "vendor_shared_package"),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.F", 120078, "third_party_shared_package"),
+                constructPackageManagerPackageInfo("vndr_pkg.G", 126345, "vendor_package.shared",
+                        ApplicationInfo.FLAG_SYSTEM, 0),
+                /*
+                 * A 3p package pretending to be a vendor package because 3p packages won't have the
+                 * required flags.
+                 */
+                constructPackageManagerPackageInfo("vendor_package.imposter", 123456, null, 0, 0)));
+
+        int[] uids = new int[]{110034, 110035, 120078, 126345, 123456};
+        List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
+                uids, Arrays.asList("vendor_package.", "vendor_pkg.", "shared:vendor_package."));
+
         List<PackageInfo> expectedPackageInfos = Arrays.asList(
-                constructPackageInfo("vendor.package.D", 110034, new ArrayList<>(),
+                constructPackageInfo("vendor_package.A", 110034, new ArrayList<>(),
                         UidType.APPLICATION, ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
-                constructPackageInfo("shared:vendor.package", 110035,
-                        Arrays.asList("vendor.pkg.E", "third_party.package.F",
-                                "third_party.package.G"), UidType.APPLICATION,
+                constructPackageInfo("shared:vendor_shared_package", 110035,
+                        Arrays.asList("vendor_pkg.B", "third_party_package.C",
+                                "third_party_package.D"), UidType.APPLICATION,
                         ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
-                constructPackageInfo("vendor.package.imposter", 123456,
-                        new ArrayList<>(), UidType.APPLICATION, ComponentType.THIRD_PARTY,
-                        ApplicationCategoryType.OTHERS),
-                constructPackageInfo("third_party.package.H", 120078,
+                constructPackageInfo("shared:third_party_shared_package", 120078,
+                        Collections.singletonList("third_party_package.F"), UidType.APPLICATION,
+                        ComponentType.THIRD_PARTY, ApplicationCategoryType.OTHERS),
+                constructPackageInfo("shared:vendor_package.shared", 126345,
+                        Collections.singletonList("vndr_pkg.G"), UidType.APPLICATION,
+                        ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
+                constructPackageInfo("vendor_package.imposter", 123456,
                         new ArrayList<>(), UidType.APPLICATION, ComponentType.THIRD_PARTY,
                         ApplicationCategoryType.OTHERS));
 
-        for (PackageInfo packageInfo : expectedPackageInfos) {
-            mPackageNamesByUids.put(packageInfo.packageIdentifier.uid,
-                    packageInfo.packageIdentifier.name);
-            mSharedPackagesByUids.put(packageInfo.packageIdentifier.uid,
-                    packageInfo.sharedUidPackages.toArray(new String[0]));
-        }
-
-        mApplicationInfosByPackages.put("vendor.package.D", constructApplicationInfo(0,
-                ApplicationInfo.PRIVATE_FLAG_PRODUCT));
-        mApplicationInfosByPackages.put("vendor.pkg.E",
-                constructApplicationInfo(ApplicationInfo.FLAG_SYSTEM, 0));
-        /*
-         * A 3p package pretending to be a vendor package because 3p packages won't have the
-         * required flags.
-         */
-        mApplicationInfosByPackages.put("vendor.package.imposter", constructApplicationInfo(0, 0));
-        mApplicationInfosByPackages.put("third_party.package.F", constructApplicationInfo(0, 0));
-        mApplicationInfosByPackages.put("third_party.package.G", constructApplicationInfo(0, 0));
-        mApplicationInfosByPackages.put("third_party.package.H", constructApplicationInfo(0, 0));
-
-        List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
-                uids, Arrays.asList("vendor.package.", "vendor.pkg."));
-
         assertPackageInfoEquals(actualPackageInfos, expectedPackageInfos);
     }
 
     @Test
     public void testGetPackageInfosForUidsWithMissingApplicationInfos() throws Exception {
+        injectPackageInfos(Arrays.asList(
+                constructPackageManagerPackageInfo(
+                        "vendor_package.A", 110034, null, 0, ApplicationInfo.PRIVATE_FLAG_OEM),
+                constructPackageManagerPackageInfo("vendor_package.B", 110035,
+                        "vendor_shared_package", 0, ApplicationInfo.PRIVATE_FLAG_VENDOR),
+                constructPackageManagerPackageInfo(
+                        "third_party_package.C", 110035, "vendor_shared_package")));
+
+        BiConsumer<Integer, String> addPackageToSharedUid = (uid, packageName) -> {
+            List<String> packages = mPackagesBySharedUid.get(uid);
+            if (packages == null) {
+                packages = new ArrayList<>();
+            }
+            packages.add(packageName);
+            mPackagesBySharedUid.put(uid, packages);
+        };
+
+        addPackageToSharedUid.accept(110035, "third_party.package.G");
+        mGenericPackageNameByUid.put(120056, "third_party.package.H");
+        mGenericPackageNameByUid.put(120078, "shared:third_party_shared_package");
+        addPackageToSharedUid.accept(120078, "third_party_package.I");
+
+
         int[] uids = new int[]{110034, 110035, 120056, 120078};
+
+        List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
+                uids, new ArrayList<>());
+
         List<PackageInfo> expectedPackageInfos = Arrays.asList(
-                constructPackageInfo("vendor.package.D", 110034, new ArrayList<>(),
+                constructPackageInfo("vendor_package.A", 110034, new ArrayList<>(),
                         UidType.APPLICATION, ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
-                constructPackageInfo("shared:vendor.package", 110035,
-                        Arrays.asList("vendor.package.E", "third_party.package.F",
+                constructPackageInfo("shared:vendor_shared_package", 110035,
+                        Arrays.asList("vendor_package.B", "third_party_package.C",
                                 "third_party.package.G"),
                         UidType.APPLICATION, ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
                 constructPackageInfo("third_party.package.H", 120056, new ArrayList<>(),
                         UidType.APPLICATION, ComponentType.UNKNOWN,
                         ApplicationCategoryType.OTHERS),
-                constructPackageInfo("shared:third_party.package", 120078,
-                        Collections.singletonList("third_party.package.I"),
+                constructPackageInfo("shared:third_party_shared_package", 120078,
+                        Collections.singletonList("third_party_package.I"),
                         UidType.APPLICATION, ComponentType.UNKNOWN,
                         ApplicationCategoryType.OTHERS));
 
-        for (PackageInfo packageInfo : expectedPackageInfos) {
-            mPackageNamesByUids.put(packageInfo.packageIdentifier.uid,
-                    packageInfo.packageIdentifier.name);
-            mSharedPackagesByUids.put(packageInfo.packageIdentifier.uid,
-                    packageInfo.sharedUidPackages.toArray(new String[0]));
-        }
-
-        mApplicationInfosByPackages.put("vendor.package.D", constructApplicationInfo(0,
-                ApplicationInfo.PRIVATE_FLAG_VENDOR));
-        mApplicationInfosByPackages.put("vendor.package.E", constructApplicationInfo(0,
-                ApplicationInfo.PRIVATE_FLAG_VENDOR));
-        mApplicationInfosByPackages.put("third_party.package.F", constructApplicationInfo(0, 0));
-
-        List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
-                uids, new ArrayList<>());
-
         assertPackageInfoEquals(actualPackageInfos, expectedPackageInfos);
     }
 
+    @Test
+    public void testSetProcessHealthCheckEnabled() throws Exception {
+        mCarWatchdogService.controlProcessHealthCheck(true);
+
+        verify(mMockCarWatchdogDaemon).controlProcessHealthCheck(eq(true));
+    }
+
+    @Test
+    public void testSetProcessHealthCheckEnabledWithDisconnectedDaemon() throws Exception {
+        crashWatchdogDaemon();
+
+        assertThrows(IllegalStateException.class,
+                () -> mCarWatchdogService.controlProcessHealthCheck(false));
+
+        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 mockPackageManager() throws Exception {
-        mPackageNamesByUids.clear();
-        mSharedPackagesByUids.clear();
-        mApplicationInfosByPackages.clear();
-        when(mMockPackageManager.getNamesForUids(any())).thenAnswer(args -> {
-            int[] uids = args.getArgument(0);
-            String[] packageNames = new String[uids.length];
-            for (int i = 0; i < uids.length; ++i) {
-                packageNames[i] = mPackageNamesByUids.get(uids[i], null);
-            }
-            return packageNames;
+    private void mockWatchdogStorage() {
+        when(mMockWatchdogStorage.saveUserPackageSettings(any())).thenAnswer((args) -> {
+            mUserPackageSettingsEntries.addAll(args.getArgument(0));
+            return true;
         });
-        when(mMockPackageManager.getPackagesForUid(anyInt())).thenAnswer(
-                args -> mSharedPackagesByUids.get(args.getArgument(0), null));
+        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);
+    }
 
-        when(mMockPackageManager.getApplicationInfoAsUser(any(), anyInt(), anyInt())).thenAnswer(
-                args -> {
-                    String packageName = args.getArgument(0);
-                    ApplicationInfo applicationInfo = mApplicationInfosByPackages
-                            .getOrDefault(packageName, /* defaultValue= */ null);
-                    if (applicationInfo == null) {
-                        throw new PackageManager.NameNotFoundException(
-                                "Package " + packageName + " not found exception");
-                    }
-                    return applicationInfo;
-                });
+    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() {
@@ -1286,38 +2353,10 @@
                 .registerReceiverForAllUsers(receiverArgumentCaptor.capture(), any(), any(), any());
         mBroadcastReceiver = receiverArgumentCaptor.getValue();
         assertWithMessage("Broadcast receiver must be non-null").that(mBroadcastReceiver)
-                .isNotEqualTo(null);
+                .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();
-    }
-
-    public void crashWatchdogDaemon() {
-        doReturn(null).when(() -> ServiceManager.getService(CAR_WATCHDOG_DAEMON_INTERFACE));
-        mCarWatchdogDaemonBinderDeathRecipient.binderDied();
-    }
-
-    public void restartWatchdogDaemonAndAwait() throws Exception {
-        CountDownLatch latch = new CountDownLatch(1);
-        doAnswer(args -> {
-            latch.countDown();
-            return null;
-        }).when(mMockBinder).linkToDeath(any(), anyInt());
-        mockWatchdogDaemon();
-        latch.await(MAX_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
-    }
-
-    private void setupUsers() {
-        when(mMockContext.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager);
-        mockUmGetAllUsers(mMockUserManager, new UserInfo[0]);
-    }
-
-    private ICarWatchdogServiceForSystem registerCarWatchdogService() throws Exception {
+    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.
          */
@@ -1327,9 +2366,147 @@
                 ArgumentCaptor.forClass(ICarWatchdogServiceForSystem.class);
         verify(mMockCarWatchdogDaemon, atLeastOnce()).registerCarWatchdogService(
                 watchdogServiceForSystemImplCaptor.capture());
-        return watchdogServiceForSystemImplCaptor.getValue();
+        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 {
+        when(mMockPackageManager.getNamesForUids(any())).thenAnswer(args -> {
+            int[] uids = args.getArgument(0);
+            String[] names = new String[uids.length];
+            for (int i = 0; i < uids.length; ++i) {
+                names[i] = mGenericPackageNameByUid.get(uids[i], null);
+            }
+            return names;
+        });
+        when(mMockPackageManager.getPackagesForUid(anyInt())).thenAnswer(args -> {
+            int uid = args.getArgument(0);
+            List<String> packages = mPackagesBySharedUid.get(uid);
+            return packages.toArray(new String[0]);
+        });
+        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenAnswer(args -> {
+                    int userId = args.getArgument(2);
+                    String userPackageId = userId + ":" + args.getArgument(0);
+                    android.content.pm.PackageInfo packageInfo =
+                            mPmPackageInfoByUserPackage.get(userPackageId);
+                    if (packageInfo == null) {
+                        throw new PackageManager.NameNotFoundException(
+                                "User package id '" + userPackageId + "' not found");
+                    }
+                    return packageInfo.applicationInfo;
+                });
+        when(mMockPackageManager.getPackageInfoAsUser(anyString(), anyInt(), anyInt())).thenAnswer(
+                args -> {
+                    int userId = args.getArgument(2);
+                    String userPackageId = userId + ":" + args.getArgument(0);
+                    android.content.pm.PackageInfo packageInfo =
+                            mPmPackageInfoByUserPackage.get(userPackageId);
+                    if (packageInfo == null) {
+                        throw new PackageManager.NameNotFoundException(
+                                "User package id '" + userPackageId + "' not found");
+                    }
+                    return packageInfo;
+                });
+        when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt())).thenAnswer(
+                args -> {
+                    int userId = args.getArgument(1);
+                    List<android.content.pm.PackageInfo> packageInfos = new ArrayList<>();
+                    for (android.content.pm.PackageInfo packageInfo :
+                            mPmPackageInfoByUserPackage.values()) {
+                        if (UserHandle.getUserId(packageInfo.applicationInfo.uid) == userId) {
+                            packageInfos.add(packageInfo);
+                        }
+                    }
+                    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 crashWatchdogDaemon() {
+        doReturn(null).when(() -> ServiceManager.getService(CAR_WATCHDOG_DAEMON_INTERFACE));
+        mCarWatchdogDaemonBinderDeathRecipient.binderDied();
+        mIsDaemonCrashed = true;
+    }
+
+    private void restartWatchdogDaemonAndAwait() throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        doAnswer(args -> {
+            latch.countDown();
+            return null;
+        }).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 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 wantedInvocations)
+            throws Exception {
+        /*
+         * Syncing the resource configuration in the service with the daemon is done on the main
+         * thread. To ensure the sync completes before verification, execute an empty block on the
+         * main thread.
+         */
+        CarServiceUtils.runOnMainSync(() -> {});
+        verify(mMockCarWatchdogDaemon, times(wantedInvocations)).getResourceOveruseConfigurations();
+    }
+
+
     private void testClientHealthCheck(TestClient client, int badClientCount) throws Exception {
         mCarWatchdogService.registerClient(client, TIMEOUT_CRITICAL);
         mWatchdogServiceForSystemImpl.checkIfAlive(123456, TIMEOUT_CRITICAL);
@@ -1352,54 +2529,80 @@
         return resourceOveruseConfigurationsCaptor.getValue();
     }
 
-    private void injectIoOveruseStatsForPackages(SparseArray<String> packageNamesByUid,
-            Set<String> killablePackages, Set<String> shouldNotifyPackages) throws Exception {
+    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();
+
+        /* Method should be invoked 2 times. Once at test setup and once more after the daemon
+         * crashes and reconnects.
+         */
+        verify(mMockCarWatchdogDaemon, times(2)).getResourceOveruseConfigurations();
+    }
+
+    private SparseArray<PackageIoOveruseStats> injectIoOveruseStatsForPackages(
+            SparseArray<String> genericPackageNameByUid, Set<String> killablePackages,
+            Set<String> shouldNotifyPackages) throws Exception {
+        SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid = new SparseArray<>();
         List<PackageIoOveruseStats> packageIoOveruseStats = new ArrayList<>();
-        for (int i = 0; i < packageNamesByUid.size(); ++i) {
-            String packageName = packageNamesByUid.valueAt(i);
-            int uid = packageNamesByUid.keyAt(i);
-            packageIoOveruseStats.add(constructPackageIoOveruseStats(uid,
-                    shouldNotifyPackages.contains(packageName),
-                    constructInternalIoOveruseStats(killablePackages.contains(packageName),
-                            /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
-                            /* writtenBytes= */constructPerStateBytes(100, 200, 300),
-                            /* totalOveruses= */2)));
+        for (int i = 0; i < genericPackageNameByUid.size(); ++i) {
+            String name = genericPackageNameByUid.valueAt(i);
+            int uid = genericPackageNameByUid.keyAt(i);
+            PackageIoOveruseStats stats = constructPackageIoOveruseStats(uid,
+                    shouldNotifyPackages.contains(name),
+                    constructInternalIoOveruseStats(killablePackages.contains(name),
+                            /* 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;
     }
 
-    private void injectPackageInfos(List<String> packageNames) {
-        List<android.content.pm.PackageInfo> packageInfos = new ArrayList<>();
-        TriConsumer<String, Integer, Integer> addPackageInfo =
-                (packageName, flags, privateFlags) -> {
-                    android.content.pm.PackageInfo packageInfo =
-                            new android.content.pm.PackageInfo();
-                    packageInfo.packageName = packageName;
-                    packageInfo.applicationInfo = new ApplicationInfo();
-                    packageInfo.applicationInfo.flags = flags;
-                    packageInfo.applicationInfo.privateFlags = privateFlags;
-                    packageInfos.add(packageInfo);
-                };
-        for (String packageName : packageNames) {
-            if (packageName.startsWith("system")) {
-                addPackageInfo.accept(packageName, ApplicationInfo.FLAG_SYSTEM, 0);
-            } else if (packageName.startsWith("vendor")) {
-                addPackageInfo.accept(packageName, ApplicationInfo.FLAG_SYSTEM,
-                        ApplicationInfo.PRIVATE_FLAG_OEM);
-            } else {
-                addPackageInfo.accept(packageName, 0, 0);
+    private void injectPackageInfos(
+            List<android.content.pm.PackageInfo> packageInfos) {
+        for (android.content.pm.PackageInfo packageInfo : packageInfos) {
+            String genericPackageName = packageInfo.packageName;
+            int uid = packageInfo.applicationInfo.uid;
+            int userId = UserHandle.getUserId(uid);
+            if (packageInfo.sharedUserId != null) {
+                genericPackageName =
+                        PackageInfoHandler.SHARED_PACKAGE_PREFIX + packageInfo.sharedUserId;
+                List<String> packages = mPackagesBySharedUid.get(uid);
+                if (packages == null) {
+                    packages = new ArrayList<>();
+                }
+                packages.add(packageInfo.packageName);
+                mPackagesBySharedUid.put(uid, packages);
             }
+            String userPackageId = userId + ":" + packageInfo.packageName;
+            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 '%s'",
+                    uid).that(mGenericPackageNameByUid.get(uid, genericPackageName))
+                    .isEqualTo(genericPackageName);
+            mPmPackageInfoByUserPackage.put(userPackageId, packageInfo);
+            mGenericPackageNameByUid.put(uid, genericPackageName);
         }
-        when(mMockPackageManager.getInstalledPackagesAsUser(eq(0), anyInt()))
-                .thenReturn(packageInfos);
     }
 
-    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)
@@ -1407,9 +2610,14 @@
         ArgumentCaptor<List<PackageResourceOveruseAction>> resourceOveruseActionsCaptor =
                 ArgumentCaptor.forClass((Class) List.class);
 
-        verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).actionTakenOnResourceOveruse(
+        verify(mMockCarWatchdogDaemon,
+                timeout(MAX_WAIT_TIME_MS).times(2)).actionTakenOnResourceOveruse(
                 resourceOveruseActionsCaptor.capture());
-        List<PackageResourceOveruseAction> actual = resourceOveruseActionsCaptor.getValue();
+        List<PackageResourceOveruseAction> actual = new ArrayList<>();
+        for (List<PackageResourceOveruseAction> actions :
+                resourceOveruseActionsCaptor.getAllValues()) {
+            actual.addAll(actions);
+        }
 
         assertThat(actual).comparingElementsUsing(
                 Correspondence.from(
@@ -1436,10 +2644,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,
@@ -1464,11 +2668,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);
@@ -1478,28 +2684,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);
@@ -1508,15 +2714,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);
 
@@ -1529,23 +2736,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;
     }
 
@@ -1578,22 +2786,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;
@@ -1601,16 +2820,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();
@@ -1622,6 +2831,24 @@
         return action;
     }
 
+    private static void delayedRunOnMainSync(Runnable action, long delayMillis)
+            throws InterruptedException {
+        AtomicBoolean isComplete = new AtomicBoolean();
+        Handler handler = new Handler(Looper.getMainLooper());
+        handler.postDelayed(() -> {
+            action.run();
+            synchronized (action) {
+                isComplete.set(true);
+                action.notifyAll();
+            }
+        }, delayMillis);
+        synchronized (action) {
+            while (!isComplete.get()) {
+                action.wait();
+            }
+        }
+    }
+
     private class TestClient extends ICarWatchdogServiceCallback.Stub {
         protected int mLastSessionId = INVALID_SESSION_ID;
 
@@ -1718,7 +2945,7 @@
 
     private static boolean isPackageInfoEquals(PackageInfo lhs, PackageInfo rhs) {
         return isEquals(lhs.packageIdentifier, rhs.packageIdentifier)
-                && lhs.sharedUidPackages.equals(rhs.sharedUidPackages)
+                && lhs.sharedUidPackages.containsAll(rhs.sharedUidPackages)
                 && lhs.componentType == rhs.componentType
                 && lhs.appCategoryType == rhs.appCategoryType;
     }
@@ -1726,4 +2953,31 @@
     private static boolean isEquals(PackageIdentifier lhs, PackageIdentifier rhs) {
         return lhs.name.equals(rhs.name) && lhs.uid == rhs.uid;
     }
+
+    private static android.content.pm.PackageInfo constructPackageManagerPackageInfo(
+            String packageName, int uid, String sharedUserId) {
+        if (packageName.startsWith("system")) {
+            return constructPackageManagerPackageInfo(
+                    packageName, uid, sharedUserId, ApplicationInfo.FLAG_SYSTEM, 0);
+        }
+        if (packageName.startsWith("vendor")) {
+            return constructPackageManagerPackageInfo(
+                    packageName, uid, sharedUserId, ApplicationInfo.FLAG_SYSTEM,
+                    ApplicationInfo.PRIVATE_FLAG_OEM);
+        }
+        return constructPackageManagerPackageInfo(packageName, uid, sharedUserId, 0, 0);
+    }
+
+    private static android.content.pm.PackageInfo constructPackageManagerPackageInfo(
+            String packageName, int uid, String sharedUserId, int flags, int privateFlags) {
+        android.content.pm.PackageInfo packageInfo = new android.content.pm.PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.sharedUserId = sharedUserId;
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.packageName = packageName;
+        packageInfo.applicationInfo.uid = uid;
+        packageInfo.applicationInfo.flags = flags;
+        packageInfo.applicationInfo.privateFlags = privateFlags;
+        return packageInfo;
+    }
 }
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 6b7b7ba..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;
@@ -71,7 +77,7 @@
         if (actual == null || expected == null) {
             return false;
         }
-        return actual.getPackageName() == expected.getPackageName()
+        return actual.getPackageName().equals(expected.getPackageName())
                 && actual.getUserHandle().equals(expected.getUserHandle())
                 && IoOveruseStatsSubject.isEquals(actual.getIoOveruseStats(),
                     expected.getIoOveruseStats());
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..3f16750
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
@@ -0,0 +1,502 @@
+/*
+ * 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 testDeleteUserPackage() throws Exception {
+        ArrayList<WatchdogStorage.UserPackageSettingsEntry> settingsEntries = sampleSettings();
+        List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = sampleStatsForToday();
+
+        assertThat(mService.saveUserPackageSettings(settingsEntries)).isTrue();
+        assertThat(mService.saveIoUsageStats(ioUsageStatsEntries)).isTrue();
+
+        int deleteUserId = 100;
+        String deletePackageName = "system_package.non_critical.A";
+
+        mService.deleteUserPackage(deleteUserId, deletePackageName);
+
+        settingsEntries.removeIf(
+                (s) -> s.userId == deleteUserId && s.packageName.equals(deletePackageName));
+
+        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+                .containsExactlyElementsIn(settingsEntries);
+
+        ioUsageStatsEntries.removeIf(
+                (e) -> e.userId == deleteUserId && e.packageName.equals(deletePackageName));
+
+        IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+                .containsExactlyElementsIn(ioUsageStatsEntries);
+    }
+
+    @Test
+    public void testDeleteUserPackageWithNonexistentPackage() throws Exception {
+        injectSampleUserPackageSettings();
+        List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = sampleStatsForToday();
+
+        assertThat(mService.saveIoUsageStats(ioUsageStatsEntries)).isTrue();
+
+        int deleteUserId = 100;
+        String deletePackageName = "system_package.non_existent.A";
+
+        mService.deleteUserPackage(deleteUserId, deletePackageName);
+
+        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+                .containsExactlyElementsIn(sampleSettings());
+
+        ioUsageStatsEntries.removeIf(
+                (e) -> e.userId == deleteUserId && e.packageName.equals(deletePackageName));
+
+        IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+                .containsExactlyElementsIn(ioUsageStatsEntries);
+    }
+
+    @Test
+    public void testDeleteUserPackageWithHistoricalIoOveruseStats()
+            throws Exception {
+        ArrayList<WatchdogStorage.UserPackageSettingsEntry> settingsEntries = sampleSettings();
+
+        assertThat(mService.saveUserPackageSettings(settingsEntries)).isTrue();
+        assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+                /* includingStartDaysAgo= */ 1, /* excludingEndDaysAgo= */ 6))).isTrue();
+
+        int deleteUserId = 100;
+        String deletePackageName = "system_package.non_critical.A";
+
+        mService.deleteUserPackage(deleteUserId, deletePackageName);
+
+        settingsEntries.removeIf(
+                (s) -> s.userId == deleteUserId && s.packageName.equals(deletePackageName));
+
+        UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+                .containsExactlyElementsIn(settingsEntries);
+
+        IoOveruseStats actual = mService.getHistoricalIoOveruseStats(
+                /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
+
+        assertWithMessage("Fetched historical I/O overuse stats").that(actual).isNull();
+    }
+
+    @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 ArrayList<WatchdogStorage.UserPackageSettingsEntry> sampleSettings() {
+        return new ArrayList<>(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 ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsBetweenDates(
+            int includingStartDaysAgo, int excludingEndDaysAgo) {
+        ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
+                .truncatedTo(STATS_TEMPORAL_UNIT);
+        ArrayList<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
+        for (int i = includingStartDaysAgo; i < excludingEndDaysAgo; ++i) {
+            entries.addAll(sampleStatsForDate(
+                    currentDate.minus(i, STATS_TEMPORAL_UNIT).toEpochSecond(),
+                    STATS_TEMPORAL_UNIT.getDuration().toSeconds()));
+        }
+        return entries;
+    }
+
+    private ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsForToday() {
+        long currentTime = mTimeSource.now().atZone(ZONE_OFFSET)
+                .truncatedTo(STATS_TEMPORAL_UNIT).toEpochSecond();
+        long duration = mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - currentTime;
+        return sampleStatsForDate(currentTime, duration);
+    }
+
+    private static ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsForDate(
+            long statsDateEpoch, long duration) {
+        ArrayList<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
+        for (int i = 100; i < 101; ++i) {
+            entries.add(constructIoUsageStatsEntry(
+                    /* userId= */ i, "system_package.non_critical.A", statsDateEpoch, duration,
+                    /* remainingWriteBytes= */
+                    CarWatchdogServiceUnitTest.constructPerStateBytes(200, 300, 400),
+                    /* 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,
+    }
+}
diff --git a/tests/common_utils/src/com/android/car/test/FakeSharedPreferences.java b/tests/common_utils/src/com/android/car/test/FakeSharedPreferences.java
new file mode 100644
index 0000000..ac85e52
--- /dev/null
+++ b/tests/common_utils/src/com/android/car/test/FakeSharedPreferences.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.test;
+
+import android.content.SharedPreferences;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/** Fake class for {@link SharedPreferences} to be used in tests. */
+public class FakeSharedPreferences implements SharedPreferences, SharedPreferences.Editor {
+    private final HashMap<String, Object> mValues = new HashMap<>();
+    private final HashMap<String, Object> mTempValues = new HashMap<>();
+
+    @Override
+    public Editor edit() {
+        return this;
+    }
+
+    @Override
+    public boolean contains(String key) {
+        return mValues.containsKey(key);
+    }
+
+    @Override
+    public Map<String, ?> getAll() {
+        return new HashMap<>(mValues);
+    }
+
+    @Override
+    public boolean getBoolean(String key, boolean defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Boolean) mValues.get(key)).booleanValue();
+        }
+        return defValue;
+    }
+
+    @Override
+    public float getFloat(String key, float defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Float) mValues.get(key)).floatValue();
+        }
+        return defValue;
+    }
+
+    @Override
+    public int getInt(String key, int defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Integer) mValues.get(key)).intValue();
+        }
+        return defValue;
+    }
+
+    @Override
+    public long getLong(String key, long defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Long) mValues.get(key)).longValue();
+        }
+        return defValue;
+    }
+
+    @Override
+    public String getString(String key, String defValue) {
+        if (mValues.containsKey(key)) {
+            return (String) mValues.get(key);
+        }
+        return defValue;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Set<String> getStringSet(String key, Set<String> defValues) {
+        if (mValues.containsKey(key)) {
+            return (Set<String>) mValues.get(key);
+        }
+        return defValues;
+    }
+
+    @Override
+    public void registerOnSharedPreferenceChangeListener(
+            OnSharedPreferenceChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void unregisterOnSharedPreferenceChangeListener(
+            OnSharedPreferenceChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Editor putBoolean(String key, boolean value) {
+        mTempValues.put(key, Boolean.valueOf(value));
+        return this;
+    }
+
+    @Override
+    public Editor putFloat(String key, float value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    @Override
+    public Editor putInt(String key, int value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    @Override
+    public Editor putLong(String key, long value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    @Override
+    public Editor putString(String key, String value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    @Override
+    public Editor putStringSet(String key, Set<String> values) {
+        mTempValues.put(key, values);
+        return this;
+    }
+
+    @Override
+    public Editor remove(String key) {
+        mTempValues.remove(key);
+        return this;
+    }
+
+    @Override
+    public Editor clear() {
+        mTempValues.clear();
+        return this;
+    }
+
+    @Override
+    public boolean commit() {
+        mValues.clear();
+        mValues.putAll(mTempValues);
+        return true;
+    }
+
+    @Override
+    public void apply() {
+        commit();
+    }
+}