Merge "Computepipe runner engine" into rvc-dev
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..6239f78
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "auto-postsubmit": [
+    {
+      "name": "AndroidCarApiTest",
+      "options": [
+        {
+          "include-filter": "android.car.apitest.PreInstalledPackagesTest"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags b/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
index 60a174d..c919397 100644
--- a/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
+++ b/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
@@ -34,9 +34,9 @@
 150008 car_helper_svc_connected (pending_operations|1)
 150009 car_helper_hal_request (request_type|1)
 150010 car_helper_hal_response (result_code|1)
-150011 car_helper_hal_default_behavior (fallback|1)
-150012 car_helper_hal_start_user (user_id|1)
-150013 car_helper_hal_create_user (flags|1), (safe_name|3)
+150011 car_helper_hal_default_behavior (fallback|1),(user_locales|3)
+150012 car_helper_hal_start_user (user_id|1),(user_locales|3)
+150013 car_helper_hal_create_user (flags|1),(safe_name|3),(user_locales|3)
 150014 car_helper_pre_creation_requested (number_users|1),(number_guests|1)
 150015 car_helper_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)
 
@@ -65,7 +65,7 @@
 #### User-related tags (range 150100 - 150199)
 
 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)
+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)
@@ -75,9 +75,12 @@
 150109 car_user_svc_get_user_auth_req (uid|1),(user_id|1),(number_types|1)
 150110 car_user_svc_get_user_auth_resp (number_values|1)
 150111 car_user_svc_switch_user_ui_req (user_id|1)
+150112 car_user_svc_switch_user_from_hal_req (request_id|1),(uid|1)
+150113 car_user_svc_set_user_auth_req (uid|1),(user_id|1),(number_associations|1)
+150114 car_user_svc_set_user_auth_resp (number_values|1),(error_message|3)
 
 150140 car_user_hal_initial_user_info_req (request_id|1),(request_type|1),(timeout|1)
-150141 car_user_hal_initial_user_info_resp (request_id|1),(status|1),(action|1),(user_id|1),(flags|1),(safe_name|3)
+150141 car_user_hal_initial_user_info_resp (request_id|1),(status|1),(action|1),(user_id|1),(flags|1),(safe_name|3),(user_locales|3)
 150142 car_user_hal_switch_user_req (request_id|1),(user_id|1),(timeout|1)
 150143 car_user_hal_switch_user_resp (request_id|1),(status|1),(result|1),(error_message|3)
 150144 car_user_hal_post_switch_user_req (request_id|1),(target_user_id|1),(current_user_id|1)
@@ -86,6 +89,7 @@
 150147 car_user_hal_legacy_switch_user_req (request_id|1),(target_user_id|1),(current_user_id|1)
 150148 car_user_hal_set_user_auth_req (int32values|4)
 150149 car_user_hal_set_user_auth_resp (int32values|4),(error_message|3)
+150150 car_user_hal_oem_switch_user_req (request_id|1),(target_user_id|1)
 
 150171 car_user_mgr_add_listener (uid|1)
 150172 car_user_mgr_remove_listener (uid|1)
@@ -94,3 +98,5 @@
 150175 car_user_mgr_switch_user_response (uid|1),(status|1),(error_message|3)
 150176 car_user_mgr_get_user_auth_req (types|4)
 150177 car_user_mgr_get_user_auth_resp (values|4)
+150178 car_user_mgr_set_user_auth_req (types_and_values_pairs|4)
+150179 car_user_mgr_set_user_auth_resp (values|4)
diff --git a/car-lib/src/android/car/CarBugreportManager.java b/car-lib/src/android/car/CarBugreportManager.java
index 68c11d3..1d7f862 100644
--- a/car-lib/src/android/car/CarBugreportManager.java
+++ b/car-lib/src/android/car/CarBugreportManager.java
@@ -161,16 +161,16 @@
      * bugreport and makes them available through a extra output file. Currently the extra
      * output contains the screenshots for all the physical displays.
      *
-     * <p>The file descriptor is closed when bugreport is written or if an exception happens.
+     * <p>It closes provided file descriptors. The callback runs on a background thread.
      *
      * <p>This method is enabled only for one bug reporting app. It can be configured using
      * {@code config_car_bugreport_application} string that is defined in
      * {@code packages/services/Car/service/res/values/config.xml}. To learn more please
      * see {@code packages/services/Car/tests/BugReportApp/README.md}.
      *
-     * @param output the zipped bugreport file
+     * @param output the zipped bugreport file.
      * @param extraOutput a zip file that contains extra files generated for automotive.
-     * @param callback  the callback for reporting dump status
+     * @param callback the callback for reporting dump status.
      */
     @RequiresPermission(android.Manifest.permission.DUMP)
     public void requestBugreport(
@@ -187,11 +187,27 @@
         } catch (RemoteException e) {
             handleRemoteExceptionFromCarService(e);
         } finally {
+            // Safely close the FDs on this side, because binder dups them.
             IoUtils.closeQuietly(output);
             IoUtils.closeQuietly(extraOutput);
         }
     }
 
+    /**
+     * Cancels the running bugreport. It doesn't guarantee immediate cancellation and after
+     * calling this method, callbacks provided in {@link #requestBugreport} might still get fired.
+     * The next {@link startBugreport} should be called after a delay to allow the system to fully
+     * complete the cancellation.
+     */
+    @RequiresPermission(android.Manifest.permission.DUMP)
+    public void cancelBugreport() {
+        try {
+            mService.cancelBugreport();
+        } catch (RemoteException e) {
+            handleRemoteExceptionFromCarService(e);
+        }
+    }
+
     @Override
     public void onCarDisconnected() {
     }
diff --git a/car-lib/src/android/car/ICarBugreportService.aidl b/car-lib/src/android/car/ICarBugreportService.aidl
index 2673c80..54ae4eb 100644
--- a/car-lib/src/android/car/ICarBugreportService.aidl
+++ b/car-lib/src/android/car/ICarBugreportService.aidl
@@ -23,7 +23,7 @@
  *
  * @hide
  */
- interface ICarBugreportService {
+interface ICarBugreportService {
 
     /**
      * Starts bugreport service to capture a zipped bugreport. The caller needs to provide
@@ -34,4 +34,9 @@
      */
     void requestBugreport(in ParcelFileDescriptor output,
             in ParcelFileDescriptor extraOutput, ICarBugreportCallback callback) = 1;
- }
+
+    /**
+     * Cancels the running bugreport.
+     */
+    void cancelBugreport() = 2;
+}
diff --git a/car-lib/src/android/car/ICarUserService.aidl b/car-lib/src/android/car/ICarUserService.aidl
index 2eccdbb..921c146 100644
--- a/car-lib/src/android/car/ICarUserService.aidl
+++ b/car-lib/src/android/car/ICarUserService.aidl
@@ -17,7 +17,7 @@
 package android.car;
 
 import android.content.pm.UserInfo;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
 import android.car.user.UserSwitchResult;
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.os.IResultReceiver;
@@ -26,16 +26,17 @@
 interface ICarUserService {
     UserInfo createDriver(in String name, boolean admin);
     UserInfo createPassenger(in String name, int driverId);
-    boolean switchDriver(int driverId);
-    oneway void switchUser(int tagerUserId, int timeoutMs,
-        in AndroidFuture<UserSwitchResult> result);
+    void switchDriver(int driverId, in AndroidFuture<UserSwitchResult> receiver);
+    void switchUser(int tagerUserId, int timeoutMs, in AndroidFuture<UserSwitchResult> receiver);
     List<UserInfo> getAllDrivers();
     List<UserInfo> getPassengers(int driverId);
     boolean startPassenger(int passengerId, int zoneId);
     boolean stopPassenger(int passengerId);
-    oneway void setLifecycleListenerForUid(in IResultReceiver listener);
-    oneway void resetLifecycleListenerForUid();
-    oneway void getInitialUserInfo(int requestType, int timeoutMs, in IResultReceiver receiver);
-    GetUserIdentificationAssociationResponse getUserIdentificationAssociation(in int[] types);
-    oneway void setUserSwitchUiCallback(in IResultReceiver callback);
+    void setLifecycleListenerForUid(in IResultReceiver listener);
+    void resetLifecycleListenerForUid();
+    void getInitialUserInfo(int requestType, int timeoutMs, in IResultReceiver receiver);
+    UserIdentificationAssociationResponse getUserIdentificationAssociation(in int[] types);
+    void setUserIdentificationAssociation(int timeoutMs, in int[] types, in int[] values,
+      in AndroidFuture<UserIdentificationAssociationResponse> result);
+    void setUserSwitchUiCallback(in IResultReceiver callback);
 }
diff --git a/car-lib/src/android/car/VehicleAreaSeat.java b/car-lib/src/android/car/VehicleAreaSeat.java
index 792ca27..611827d 100644
--- a/car-lib/src/android/car/VehicleAreaSeat.java
+++ b/car-lib/src/android/car/VehicleAreaSeat.java
@@ -16,16 +16,26 @@
 package android.car;
 
 import android.annotation.IntDef;
-import android.car.hardware.CarPropertyValue;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * VehicleAreaSeat is an abstraction for a seat in a car. Some car APIs like
- * {@link CarPropertyValue} may provide control per seat and
- * values defined here should be used to distinguish different seats.
+ * Object used to indicate the area value for car properties which have area type
+ * {@link VehicleAreaType#VEHICLE_AREA_TYPE_SEAT}.
+ * <p>
+ * The constants defined by {@link VehicleAreaSeat} indicate the position for area type
+ * {@link VehicleAreaType#VEHICLE_AREA_TYPE_SEAT}. A property can have a single or a combination of
+ * positions. Developers can query the position using
+ * {@link android.car.hardware.property.CarPropertyManager#getAreaId(int, int)}.
+ * </p><p>
+ * Refer to {@link android.car.hardware.CarPropertyConfig#getAreaIds()} for more information about
+ * areaId.
+ * </p>
  */
+
+// This class is only designed to provide constants for VehicleAreaSeat. The constants should
+// be same as VehicleAreaSeat in /hardware/interfaces/automotive/vehicle/2.0/types.hal.
 public final class VehicleAreaSeat {
     /** List of vehicle's seats. */
     public static final int SEAT_UNKNOWN = 0;
diff --git a/car-lib/src/android/car/VehicleAreaType.java b/car-lib/src/android/car/VehicleAreaType.java
index 826ffe3..5cd5c7a 100644
--- a/car-lib/src/android/car/VehicleAreaType.java
+++ b/car-lib/src/android/car/VehicleAreaType.java
@@ -16,15 +16,24 @@
 package android.car;
 
 import android.annotation.IntDef;
+import android.car.hardware.CarPropertyConfig;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * Represents vehicle area such as window, door, seat, etc.
- * See also {@link VehicleAreaDoor}, {@link VehicleAreaSeat},
- * {@link VehicleAreaWindow},
+ * Object used to indicate area types for car properties.
+ * <p>
+ * The constants defined by {@link VehicleAreaType} indicate the area types for properties.  A
+ * property only has one area type. Developers can query the area type using
+ * {@link CarPropertyConfig#getPropertyType()}.
+ * </p><p>
+ * Refer to {@link VehicleAreaSeat} and {@link VehicleAreaWheel} for more information about areaId.
+ * </p>
  */
+
+// This class is only designed to provide constants for VehicleAreaType. The constants should
+// exactly be same as VehicleAreaType in /hardware/interfaces/automotive/vehicle/2.0/types.hal.
 public final class VehicleAreaType {
     /** Used for global properties */
     public static final int VEHICLE_AREA_TYPE_GLOBAL = 0;
diff --git a/car-lib/src/android/car/VehicleOilLevel.java b/car-lib/src/android/car/VehicleOilLevel.java
index 5a21a2d..75fd1da 100644
--- a/car-lib/src/android/car/VehicleOilLevel.java
+++ b/car-lib/src/android/car/VehicleOilLevel.java
@@ -16,7 +16,7 @@
 package android.car;
 
 import android.annotation.IntDef;
-import android.annotation.SystemApi;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
diff --git a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
index be8e8c4..1167810 100644
--- a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
+++ b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
@@ -41,6 +41,7 @@
      *
      * @hide
      */
+    @Deprecated
     @SystemApi
     public static final String CATEGORY_NAVIGATION = "android.car.cluster.NAVIGATION";
 
@@ -53,6 +54,7 @@
      *
      * @hide
      */
+    @Deprecated
     @SystemApi
     public static final String KEY_EXTRA_ACTIVITY_STATE =
             "android.car.cluster.ClusterActivityState";
@@ -64,6 +66,7 @@
      *
      * @hide
      */
+    @Deprecated
     @SystemApi
     public void startActivity(Intent intent) {
         // No-op
@@ -81,6 +84,7 @@
      *
      * @hide
      */
+    @Deprecated
     @SystemApi
     public void registerCallback(String category, Callback callback) {
         // No-op
@@ -95,6 +99,7 @@
      *
      * @hide
      */
+    @Deprecated
     @SystemApi
     public void unregisterCallback(Callback callback) {
         // No-op
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderer.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderer.java
index a66258c..9687efb 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderer.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderer.java
@@ -38,15 +38,21 @@
     @Nullable private NavigationRenderer mNavigationRenderer;
 
     /**
-     * Calls once when instrument cluster should be created.
+     * Called when instrument cluster renderer is created.
      */
-    abstract public void onCreate(Context context);
+    public abstract void onCreate(Context context);
 
-    abstract public void onStart();
+    /**
+     * Called when instrument cluster renderer is started.
+     */
+    public abstract void onStart();
 
-    abstract public void onStop();
+    /**
+     * Called when instrument cluster renderer is stopped.
+     */
+    public abstract void onStop();
 
-    abstract protected NavigationRenderer createNavigationRenderer();
+    protected abstract NavigationRenderer createNavigationRenderer();
 
     /** The method is thread-safe, callers should cache returned object. */
     @Nullable
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index cfb4e44..5272548 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -52,6 +52,7 @@
 import com.android.internal.annotations.GuardedBy;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.Collection;
@@ -524,7 +525,6 @@
         @SuppressWarnings("deprecation")
         public void onNavigationStateChanged(@Nullable Bundle bundle) throws RemoteException {
             assertClusterManagerPermission();
-            assertContextOwnership();
             mUiHandler.post(() -> {
                 if (mNavigationRenderer != null) {
                     mNavigationRenderer.onNavigationStateChanged(bundle);
@@ -537,18 +537,6 @@
             assertClusterManagerPermission();
             return runAndWaitResult(() -> mNavigationRenderer.getNavigationProperties());
         }
-
-        private void assertContextOwnership() {
-            int uid = getCallingUid();
-            int pid = getCallingPid();
-
-            synchronized (mLock) {
-                if (mNavContextOwner.mUid != uid || mNavContextOwner.mPid != pid) {
-                    throw new IllegalStateException("Client {uid:" + uid + ", pid: " + pid + "} is"
-                            + " not an owner of APP_FOCUS_TYPE_NAVIGATION " + mNavContextOwner);
-                }
-            }
-        }
     }
 
     private void assertClusterManagerPermission() {
@@ -599,8 +587,8 @@
             if (uri.getQueryParameter(BITMAP_QUERY_WIDTH).isEmpty() || uri.getQueryParameter(
                     BITMAP_QUERY_HEIGHT).isEmpty()) {
                 throw new IllegalArgumentException(
-                        "Uri must have '" + BITMAP_QUERY_WIDTH + "' and '" + BITMAP_QUERY_HEIGHT
-                                + "' query parameters");
+                    "Uri must have '" + BITMAP_QUERY_WIDTH + "' and '" + BITMAP_QUERY_HEIGHT
+                            + "' query parameters");
             }
 
             ContextOwner contextOwner = getNavigationContextOwner();
@@ -610,7 +598,6 @@
             }
 
             String host = uri.getHost();
-
             if (!contextOwner.mAuthorities.contains(host)) {
                 Log.e(TAG, "Uri points to an authority not handled by the current context owner: "
                         + uri + " (valid authorities: " + contextOwner.mAuthorities + ")");
@@ -626,19 +613,19 @@
             if (Log.isLoggable(TAG, Log.DEBUG)) {
                 Log.d(TAG, "Requesting bitmap: " + uri);
             }
-            ParcelFileDescriptor fileDesc = getContentResolver()
-                    .openFileDescriptor(filteredUid, "r");
-            if (fileDesc != null) {
-                Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDesc.getFileDescriptor());
-                fileDesc.close();
-                return bitmap;
-            } else {
-                Log.e(TAG, "Failed to create pipe for uri string: " + uri);
+            try (ParcelFileDescriptor fileDesc = getContentResolver()
+                    .openFileDescriptor(filteredUid, "r")) {
+                if (fileDesc != null) {
+                    Bitmap bitmap = BitmapFactory.decodeFileDescriptor(
+                            fileDesc.getFileDescriptor());
+                    return bitmap;
+                } else {
+                    Log.e(TAG, "Failed to create pipe for uri string: " + uri);
+                }
             }
-        } catch (Throwable e) {
+        } catch (IOException e) {
             Log.e(TAG, "Unable to fetch uri: " + uri, e);
         }
-
         return null;
     }
 
@@ -708,27 +695,24 @@
                 if (Log.isLoggable(TAG, Log.DEBUG)) {
                     Log.d(TAG, "Requesting bitmap: " + uri);
                 }
-                ParcelFileDescriptor fileDesc = getContentResolver()
-                        .openFileDescriptor(filteredUid, "r");
-                if (fileDesc != null) {
-                    bitmap = BitmapFactory.decodeFileDescriptor(fileDesc.getFileDescriptor());
-                    fileDesc.close();
-                    return bitmap;
-                } else {
-                    Log.e(TAG, "Failed to create pipe for uri string: " + uri);
+                try (ParcelFileDescriptor fileDesc = getContentResolver()
+                        .openFileDescriptor(filteredUid, "r")) {
+                    if (fileDesc != null) {
+                        bitmap = BitmapFactory.decodeFileDescriptor(fileDesc.getFileDescriptor());
+                        return bitmap;
+                    } else {
+                        Log.e(TAG, "Failed to create pipe for uri string: " + uri);
+                    }
                 }
-
                 if (bitmap.getWidth() != width || bitmap.getHeight() != height) {
                     bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
                 }
                 mCache.put(uri.toString(), bitmap);
             }
-
             return bitmap;
-        } catch (Throwable e) {
+        } catch (IOException e) {
             Log.e(TAG, "Unable to fetch uri: " + uri, e);
         }
-
         return null;
     }
 }
diff --git a/car-lib/src/android/car/content/pm/CarPackageManager.java b/car-lib/src/android/car/content/pm/CarPackageManager.java
index 5d5936b..98c5822 100644
--- a/car-lib/src/android/car/content/pm/CarPackageManager.java
+++ b/car-lib/src/android/car/content/pm/CarPackageManager.java
@@ -108,8 +108,8 @@
     @SystemApi
     public void setAppBlockingPolicy(
             String packageName, CarAppBlockingPolicy policy, @SetPolicyFlags int flags) {
-        if ((flags & FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0 &&
-                Looper.getMainLooper().isCurrentThread()) {
+        if ((flags & FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0
+                && Looper.getMainLooper().isCurrentThread()) {
             throw new IllegalStateException(
                     "FLAG_SET_POLICY_WAIT_FOR_CHANGE cannot be used in main thread");
         }
diff --git a/car-lib/src/android/car/diagnostic/CarDiagnosticEvent.java b/car-lib/src/android/car/diagnostic/CarDiagnosticEvent.java
index 94abbb5..901d14e 100644
--- a/car-lib/src/android/car/diagnostic/CarDiagnosticEvent.java
+++ b/car-lib/src/android/car/diagnostic/CarDiagnosticEvent.java
@@ -50,13 +50,13 @@
      * Sparse array that contains the mapping of OBD2 diagnostic properties to their values for
      * integer valued properties
      */
-    private final SparseIntArray intValues;
+    private final SparseIntArray mIntValues;
 
     /**
      * Sparse array that contains the mapping of OBD2 diagnostic properties to their values for
      * float valued properties
      */
-    private final SparseArray<Float> floatValues;
+    private final SparseArray<Float> mFloatValues;
 
     /**
      * Diagnostic Troubleshooting Code (DTC) that was detected and caused this frame to be stored
@@ -68,18 +68,18 @@
         frameType = in.readInt();
         timestamp = in.readLong();
         int len = in.readInt();
-        floatValues = new SparseArray<>(len);
+        mFloatValues = new SparseArray<>(len);
         for (int i = 0; i < len; ++i) {
             int key = in.readInt();
             float value = in.readFloat();
-            floatValues.put(key, value);
+            mFloatValues.put(key, value);
         }
         len = in.readInt();
-        intValues = new SparseIntArray(len);
+        mIntValues = new SparseIntArray(len);
         for (int i = 0; i < len; ++i) {
             int key = in.readInt();
             int value = in.readInt();
-            intValues.put(key, value);
+            mIntValues.put(key, value);
         }
         dtc = (String) in.readValue(String.class.getClassLoader());
         // version 1 up to here
@@ -94,17 +94,17 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(frameType);
         dest.writeLong(timestamp);
-        dest.writeInt(floatValues.size());
-        for (int i = 0; i < floatValues.size(); ++i) {
-            int key = floatValues.keyAt(i);
+        dest.writeInt(mFloatValues.size());
+        for (int i = 0; i < mFloatValues.size(); ++i) {
+            int key = mFloatValues.keyAt(i);
             dest.writeInt(key);
-            dest.writeFloat(floatValues.get(key));
+            dest.writeFloat(mFloatValues.get(key));
         }
-        dest.writeInt(intValues.size());
-        for (int i = 0; i < intValues.size(); ++i) {
-            int key = intValues.keyAt(i);
+        dest.writeInt(mIntValues.size());
+        for (int i = 0; i < mIntValues.size(); ++i) {
+            int key = mIntValues.keyAt(i);
             dest.writeInt(key);
-            dest.writeInt(intValues.get(key));
+            dest.writeInt(mIntValues.get(key));
         }
         dest.writeValue(dtc);
     }
@@ -141,19 +141,19 @@
         jsonWriter.name("timestamp").value(timestamp);
 
         jsonWriter.name("intValues").beginArray();
-        for (int i = 0; i < intValues.size(); ++i) {
+        for (int i = 0; i < mIntValues.size(); ++i) {
             jsonWriter.beginObject();
-            jsonWriter.name("id").value(intValues.keyAt(i));
-            jsonWriter.name("value").value(intValues.valueAt(i));
+            jsonWriter.name("id").value(mIntValues.keyAt(i));
+            jsonWriter.name("value").value(mIntValues.valueAt(i));
             jsonWriter.endObject();
         }
         jsonWriter.endArray();
 
         jsonWriter.name("floatValues").beginArray();
-        for (int i = 0; i < floatValues.size(); ++i) {
+        for (int i = 0; i < mFloatValues.size(); ++i) {
             jsonWriter.beginObject();
-            jsonWriter.name("id").value(floatValues.keyAt(i));
-            jsonWriter.name("value").value(floatValues.valueAt(i));
+            jsonWriter.name("id").value(mFloatValues.keyAt(i));
+            jsonWriter.name("value").value(mFloatValues.valueAt(i));
             jsonWriter.endObject();
         }
         jsonWriter.endArray();
@@ -184,8 +184,8 @@
             String dtc) {
         this.frameType = frameType;
         this.timestamp = timestamp;
-        this.floatValues = floatValues;
-        this.intValues = intValues;
+        mFloatValues = floatValues;
+        mIntValues = intValues;
         this.dtc = dtc;
     }
 
@@ -308,16 +308,16 @@
      * @hide
      */
     public CarDiagnosticEvent withVendorSensorsRemoved() {
-        SparseIntArray newIntValues = intValues.clone();
-        SparseArray<Float> newFloatValues = floatValues.clone();
-        for (int i = 0; i < intValues.size(); ++i) {
-            int key = intValues.keyAt(i);
+        SparseIntArray newIntValues = mIntValues.clone();
+        SparseArray<Float> newFloatValues = mFloatValues.clone();
+        for (int i = 0; i < mIntValues.size(); ++i) {
+            int key = mIntValues.keyAt(i);
             if (key >= android.car.diagnostic.IntegerSensorIndex.LAST_SYSTEM) {
                 newIntValues.delete(key);
             }
         }
-        for (int i = 0; i < floatValues.size(); ++i) {
-            int key = floatValues.keyAt(i);
+        for (int i = 0; i < mFloatValues.size(); ++i) {
+            int key = mFloatValues.keyAt(i);
             if (key >= android.car.diagnostic.FloatSensorIndex.LAST_SYSTEM) {
                 newFloatValues.delete(key);
             }
@@ -337,8 +337,8 @@
 
     /** @hide */
     public boolean isEmptyFrame() {
-        boolean empty = (0 == intValues.size());
-        empty &= (0 == floatValues.size());
+        boolean empty = (0 == mIntValues.size());
+        empty &= (0 == mFloatValues.size());
         if (isFreezeFrame()) empty &= dtc.isEmpty();
         return empty;
     }
@@ -372,37 +372,42 @@
         if (!(otherObject instanceof CarDiagnosticEvent)) {
             return false;
         }
-        CarDiagnosticEvent otherEvent = (CarDiagnosticEvent)otherObject;
-        if (otherEvent.frameType != frameType)
+        CarDiagnosticEvent otherEvent = (CarDiagnosticEvent) otherObject;
+        if (otherEvent.frameType != frameType) {
             return false;
-        if (otherEvent.timestamp != timestamp)
+        }
+        if (otherEvent.timestamp != timestamp) {
             return false;
-        if (otherEvent.intValues.size() != intValues.size())
+        }
+        if (otherEvent.mIntValues.size() != mIntValues.size()) {
             return false;
-        if (otherEvent.floatValues.size() != floatValues.size())
+        }
+        if (otherEvent.mFloatValues.size() != mFloatValues.size()) {
             return false;
-        if (!Objects.equals(dtc, otherEvent.dtc))
+        }
+        if (!Objects.equals(dtc, otherEvent.dtc)) {
             return false;
-        for (int i = 0; i < intValues.size(); ++i) {
-            int key = intValues.keyAt(i);
-            int otherKey = otherEvent.intValues.keyAt(i);
+        }
+        for (int i = 0; i < mIntValues.size(); ++i) {
+            int key = mIntValues.keyAt(i);
+            int otherKey = otherEvent.mIntValues.keyAt(i);
             if (key != otherKey) {
                 return false;
             }
-            int value = intValues.valueAt(i);
-            int otherValue = otherEvent.intValues.valueAt(i);
+            int value = mIntValues.valueAt(i);
+            int otherValue = otherEvent.mIntValues.valueAt(i);
             if (value != otherValue) {
                 return false;
             }
         }
-        for (int i = 0; i < floatValues.size(); ++i) {
-            int key = floatValues.keyAt(i);
-            int otherKey = otherEvent.floatValues.keyAt(i);
+        for (int i = 0; i < mFloatValues.size(); ++i) {
+            int key = mFloatValues.keyAt(i);
+            int otherKey = otherEvent.mFloatValues.keyAt(i);
             if (key != otherKey) {
                 return false;
             }
-            float value = floatValues.valueAt(i);
-            float otherValue = otherEvent.floatValues.valueAt(i);
+            float value = mFloatValues.valueAt(i);
+            float otherValue = otherEvent.mFloatValues.valueAt(i);
             if (value != otherValue) {
                 return false;
             }
@@ -412,22 +417,22 @@
 
     @Override
     public int hashCode() {
-        Integer[] intKeys = new Integer[intValues.size()];
-        Integer[] floatKeys = new Integer[floatValues.size()];
+        Integer[] intKeys = new Integer[mIntValues.size()];
+        Integer[] floatKeys = new Integer[mFloatValues.size()];
         Integer[] intValues = new Integer[intKeys.length];
         Float[] floatValues = new Float[floatKeys.length];
         for (int i = 0; i < intKeys.length; ++i) {
-            intKeys[i] = this.intValues.keyAt(i);
-            intValues[i] = this.intValues.valueAt(i);
+            intKeys[i] = mIntValues.keyAt(i);
+            intValues[i] = mIntValues.valueAt(i);
         }
         for (int i = 0; i < floatKeys.length; ++i) {
-            floatKeys[i] = this.floatValues.keyAt(i);
-            floatValues[i] = this.floatValues.valueAt(i);
+            floatKeys[i] = mFloatValues.keyAt(i);
+            floatValues[i] = mFloatValues.valueAt(i);
         }
-        int intKeysHash = Objects.hash((Object[])intKeys);
-        int intValuesHash = Objects.hash((Object[])intValues);
-        int floatKeysHash = Objects.hash((Object[])floatKeys);
-        int floatValuesHash = Objects.hash((Object[])floatValues);
+        int intKeysHash = Objects.hash((Object[]) intKeys);
+        int intValuesHash = Objects.hash((Object[]) intValues);
+        int floatKeysHash = Objects.hash((Object[]) floatKeys);
+        int floatValuesHash = Objects.hash((Object[]) floatValues);
         return Objects.hash(frameType,
                 timestamp,
                 dtc,
@@ -443,13 +448,13 @@
                 "%s diagnostic frame {\n"
                         + "\ttimestamp: %d, "
                         + "DTC: %s\n"
-                        + "\tintValues: %s\n"
-                        + "\tfloatValues: %s\n}",
+                        + "\tmIntValues: %s\n"
+                        + "\tmFloatValues: %s\n}",
                 isLiveFrame() ? "live" : "freeze",
                 timestamp,
                 dtc,
-                intValues.toString(),
-                floatValues.toString());
+                mIntValues.toString(),
+                mFloatValues.toString());
     }
 
     /**
@@ -458,7 +463,7 @@
      */
     public int getSystemIntegerSensor(
             @android.car.diagnostic.IntegerSensorIndex.SensorIndex int sensor, int defaultValue) {
-        return intValues.get(sensor, defaultValue);
+        return mIntValues.get(sensor, defaultValue);
     }
 
     /**
@@ -467,7 +472,7 @@
      */
     public float getSystemFloatSensor(
             @android.car.diagnostic.FloatSensorIndex.SensorIndex int sensor, float defaultValue) {
-        return floatValues.get(sensor, defaultValue);
+        return mFloatValues.get(sensor, defaultValue);
     }
 
     /**
@@ -475,7 +480,7 @@
      * Returns defaultValue otherwise.
      */
     public int getVendorIntegerSensor(int sensor, int defaultValue) {
-        return intValues.get(sensor, defaultValue);
+        return mIntValues.get(sensor, defaultValue);
     }
 
     /**
@@ -483,7 +488,7 @@
      * Returns defaultValue otherwise.
      */
     public float getVendorFloatSensor(int sensor, float defaultValue) {
-        return floatValues.get(sensor, defaultValue);
+        return mFloatValues.get(sensor, defaultValue);
     }
 
     /**
@@ -492,9 +497,9 @@
      */
     public @Nullable Integer getSystemIntegerSensor(
             @android.car.diagnostic.IntegerSensorIndex.SensorIndex int sensor) {
-        int index = intValues.indexOfKey(sensor);
+        int index = mIntValues.indexOfKey(sensor);
         if (index < 0) return null;
-        return intValues.valueAt(index);
+        return mIntValues.valueAt(index);
     }
 
     /**
@@ -503,9 +508,9 @@
      */
     public @Nullable Float getSystemFloatSensor(
             @android.car.diagnostic.FloatSensorIndex.SensorIndex int sensor) {
-        int index = floatValues.indexOfKey(sensor);
+        int index = mFloatValues.indexOfKey(sensor);
         if (index < 0) return null;
-        return floatValues.valueAt(index);
+        return mFloatValues.valueAt(index);
     }
 
     /**
@@ -513,9 +518,9 @@
      * Returns null otherwise.
      */
     public @Nullable Integer getVendorIntegerSensor(int sensor) {
-        int index = intValues.indexOfKey(sensor);
+        int index = mIntValues.indexOfKey(sensor);
         if (index < 0) return null;
-        return intValues.valueAt(index);
+        return mIntValues.valueAt(index);
     }
 
     /**
@@ -523,9 +528,9 @@
      * Returns null otherwise.
      */
     public @Nullable Float getVendorFloatSensor(int sensor) {
-        int index = floatValues.indexOfKey(sensor);
+        int index = mFloatValues.indexOfKey(sensor);
         if (index < 0) return null;
-        return floatValues.valueAt(index);
+        return mFloatValues.valueAt(index);
     }
 
     /**
@@ -658,6 +663,9 @@
                 mIncompleteBitmask = incompleteBitmask;
             }
 
+            /**
+             * Returns the {@link IgnitionMonitor} associated with the value passed as parameter.
+             */
             public IgnitionMonitor fromValue(int value) {
                 boolean available = (0 != (value & mAvailableBitmask));
                 boolean incomplete = (0 != (value & mIncompleteBitmask));
@@ -721,8 +729,9 @@
          * Returns null otherwise.
          */
         public @Nullable CompressionIgnitionMonitors asCompressionIgnitionMonitors() {
-            if (this instanceof CompressionIgnitionMonitors)
+            if (this instanceof CompressionIgnitionMonitors) {
                 return (CompressionIgnitionMonitors) this;
+            }
             return null;
         }
     }
@@ -907,7 +916,8 @@
      * Returns null otherwise.
      */
     public @Nullable @SecondaryAirStatus.Status Integer getSecondaryAirStatus() {
-        return getSystemIntegerSensor(android.car.diagnostic.IntegerSensorIndex.COMMANDED_SECONDARY_AIR_STATUS);
+        return getSystemIntegerSensor(
+            android.car.diagnostic.IntegerSensorIndex.COMMANDED_SECONDARY_AIR_STATUS);
     }
 
     /**
@@ -916,9 +926,11 @@
      */
     public @Nullable CommonIgnitionMonitors getIgnitionMonitors() {
         Integer ignitionMonitorsType =
-                getSystemIntegerSensor(android.car.diagnostic.IntegerSensorIndex.IGNITION_MONITORS_SUPPORTED);
+                getSystemIntegerSensor(
+                    android.car.diagnostic.IntegerSensorIndex.IGNITION_MONITORS_SUPPORTED);
         Integer ignitionMonitorsBitmask =
-                getSystemIntegerSensor(android.car.diagnostic.IntegerSensorIndex.IGNITION_SPECIFIC_MONITORS);
+                getSystemIntegerSensor(
+                    android.car.diagnostic.IntegerSensorIndex.IGNITION_SPECIFIC_MONITORS);
         if (null == ignitionMonitorsType) return null;
         if (null == ignitionMonitorsBitmask) return null;
         switch (ignitionMonitorsType) {
diff --git a/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java b/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
index 8c2f8e3..9799707 100644
--- a/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
+++ b/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
@@ -54,7 +54,7 @@
     public @interface FrameType {}
 
     /** @hide */
-    public static final @FrameType int FRAME_TYPES[] = {
+    public static final @FrameType int[] FRAME_TYPES = {
         FRAME_TYPE_LIVE,
         FRAME_TYPE_FREEZE
     };
@@ -95,7 +95,7 @@
 
     @Override
     public void onCarDisconnected() {
-        synchronized(mActiveListeners) {
+        synchronized (mActiveListeners) {
             mActiveListeners.clear();
         }
     }
@@ -107,7 +107,7 @@
          *
          * @param carDiagnosticEvent
          */
-        void onDiagnosticEvent(final CarDiagnosticEvent carDiagnosticEvent);
+        void onDiagnosticEvent(CarDiagnosticEvent carDiagnosticEvent);
     }
 
     // OnDiagnosticEventListener registration
@@ -134,7 +134,7 @@
     public boolean registerListener(
             OnDiagnosticEventListener listener, @FrameType int frameType, int rate) {
         assertFrameType(frameType);
-        synchronized(mActiveListeners) {
+        synchronized (mActiveListeners) {
             boolean needsServerUpdate = false;
             CarDiagnosticListeners listeners = mActiveListeners.get(frameType);
             if (listeners == null) {
@@ -159,8 +159,8 @@
      * @param listener
      */
     public void unregisterListener(OnDiagnosticEventListener listener) {
-        synchronized(mActiveListeners) {
-            for(@FrameType int frameType : FRAME_TYPES) {
+        synchronized (mActiveListeners) {
+            for (@FrameType int frameType : FRAME_TYPES) {
                 doUnregisterListenerLocked(listener, frameType);
             }
         }
@@ -177,7 +177,7 @@
             if (listeners.isEmpty()) {
                 try {
                     mService.unregisterDiagnosticListener(frameType,
-                        mListenerToService);
+                            mListenerToService);
                 } catch (RemoteException e) {
                     handleRemoteExceptionFromCarService(e);
                     // continue for local clean-up
@@ -340,12 +340,12 @@
             extends Stub {
         private final WeakReference<CarDiagnosticManager> mManager;
 
-        public CarDiagnosticEventListenerToService(CarDiagnosticManager manager) {
+        CarDiagnosticEventListenerToService(CarDiagnosticManager manager) {
             mManager = new WeakReference<>(manager);
         }
 
         private void handleOnDiagnosticEvents(CarDiagnosticManager manager,
-            List<CarDiagnosticEvent> events) {
+                List<CarDiagnosticEvent> events) {
             manager.mHandlerCallback.sendEvents(events);
         }
 
@@ -372,8 +372,8 @@
             }
             mLastUpdateTime = updateTime;
             final boolean hasVendorExtensionPermission = mVendorExtensionPermission.checkGranted();
-            final CarDiagnosticEvent eventToDispatch = hasVendorExtensionPermission ?
-                    event :
+            final CarDiagnosticEvent eventToDispatch = hasVendorExtensionPermission
+                    ? event :
                     event.withVendorSensorsRemoved();
             List<OnDiagnosticEventListener> listeners;
             synchronized (mActiveListeners) {
diff --git a/car-lib/src/android/car/diagnostic/FloatSensorIndex.java b/car-lib/src/android/car/diagnostic/FloatSensorIndex.java
index 6ec7e8c..790d43f 100644
--- a/car-lib/src/android/car/diagnostic/FloatSensorIndex.java
+++ b/car-lib/src/android/car/diagnostic/FloatSensorIndex.java
@@ -12,12 +12,13 @@
  * WITHOUT 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.diagnostic;
 
 import android.annotation.IntDef;
 import android.annotation.SystemApi;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
diff --git a/car-lib/src/android/car/diagnostic/IntegerSensorIndex.java b/car-lib/src/android/car/diagnostic/IntegerSensorIndex.java
index 15291f7..83fd303 100644
--- a/car-lib/src/android/car/diagnostic/IntegerSensorIndex.java
+++ b/car-lib/src/android/car/diagnostic/IntegerSensorIndex.java
@@ -12,12 +12,13 @@
  * WITHOUT 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.diagnostic;
 
 import android.annotation.IntDef;
 import android.annotation.SystemApi;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
diff --git a/car-lib/src/android/car/drivingstate/CarDrivingStateEvent.java b/car-lib/src/android/car/drivingstate/CarDrivingStateEvent.java
index bd553ce..db2cf55 100644
--- a/car-lib/src/android/car/drivingstate/CarDrivingStateEvent.java
+++ b/car-lib/src/android/car/drivingstate/CarDrivingStateEvent.java
@@ -84,16 +84,19 @@
         dest.writeLong(timeStamp);
     }
 
-    public static final Parcelable.Creator<CarDrivingStateEvent> CREATOR
-            = new Parcelable.Creator<CarDrivingStateEvent>() {
-        public CarDrivingStateEvent createFromParcel(Parcel in) {
-            return new CarDrivingStateEvent(in);
-        }
+    public static final Parcelable.Creator<CarDrivingStateEvent> CREATOR =
+            new Parcelable.Creator<CarDrivingStateEvent>() {
 
-        public CarDrivingStateEvent[] newArray(int size) {
-            return new CarDrivingStateEvent[size];
-        }
-    };
+                @Override
+                public CarDrivingStateEvent createFromParcel(Parcel in) {
+                    return new CarDrivingStateEvent(in);
+                }
+
+                @Override
+                public CarDrivingStateEvent[] newArray(int size) {
+                    return new CarDrivingStateEvent[size];
+                }
+            };
 
     public CarDrivingStateEvent(int value, long time) {
         eventValue = value;
diff --git a/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java b/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
index 4053c5c..0998e7d 100644
--- a/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
+++ b/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
@@ -61,8 +61,8 @@
     /** @hide */
     @Override
     public synchronized void onCarDisconnected() {
-            mListenerToService = null;
-            mDrvStateEventListener = null;
+        mListenerToService = null;
+        mDrvStateEventListener = null;
     }
 
     /**
@@ -182,7 +182,7 @@
             ICarDrivingStateChangeListener.Stub {
         private final WeakReference<CarDrivingStateManager> mDrvStateMgr;
 
-        public CarDrivingStateChangeListenerToService(CarDrivingStateManager manager) {
+        CarDrivingStateChangeListenerToService(CarDrivingStateManager manager) {
             mDrvStateMgr = new WeakReference<>(manager);
         }
 
@@ -216,7 +216,7 @@
     private static final class EventCallbackHandler extends Handler {
         private final WeakReference<CarDrivingStateManager> mDrvStateMgr;
 
-        public EventCallbackHandler(CarDrivingStateManager manager, Looper looper) {
+        EventCallbackHandler(CarDrivingStateManager manager, Looper looper) {
             super(looper);
             mDrvStateMgr = new WeakReference<>(manager);
         }
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
index 79bd8e7..735e0e3 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
@@ -304,7 +304,7 @@
             ICarUxRestrictionsChangeListener.Stub {
         private final WeakReference<CarUxRestrictionsManager> mUxRestrictionsManager;
 
-        public CarUxRestrictionsChangeListenerToService(CarUxRestrictionsManager manager) {
+        CarUxRestrictionsChangeListenerToService(CarUxRestrictionsManager manager) {
             mUxRestrictionsManager = new WeakReference<>(manager);
         }
 
@@ -337,7 +337,7 @@
     private static final class EventCallbackHandler extends Handler {
         private final WeakReference<CarUxRestrictionsManager> mUxRestrictionsManager;
 
-        public EventCallbackHandler(CarUxRestrictionsManager manager, Looper looper) {
+        EventCallbackHandler(CarUxRestrictionsManager manager, Looper looper) {
             super(looper);
             mUxRestrictionsManager = new WeakReference<>(manager);
         }
diff --git a/car-lib/src/android/car/hardware/CarPropertyConfig.java b/car-lib/src/android/car/hardware/CarPropertyConfig.java
index a74a361..df7ac5f 100644
--- a/car-lib/src/android/car/hardware/CarPropertyConfig.java
+++ b/car-lib/src/android/car/hardware/CarPropertyConfig.java
@@ -434,8 +434,8 @@
             mMaxValue = maxValue;
         }
 
-        public static final Parcelable.Creator<AreaConfig<Object>> CREATOR
-                = getCreator(Object.class);
+        public static final Parcelable.Creator<AreaConfig<Object>> CREATOR =
+                getCreator(Object.class);
 
         private static <E> Parcelable.Creator<AreaConfig<E>> getCreator(final Class<E> clazz) {
             return new Creator<AreaConfig<E>>() {
@@ -457,8 +457,13 @@
             mMaxValue = (T) in.readValue(getClass().getClassLoader());
         }
 
-        @Nullable public T getMinValue() { return mMinValue; }
-        @Nullable public T getMaxValue() { return mMaxValue; }
+        @Nullable public T getMinValue() {
+            return mMinValue;
+        }
+
+        @Nullable public T getMaxValue() {
+            return mMaxValue;
+        }
 
         @Override
         public int describeContents() {
@@ -473,10 +478,10 @@
 
         @Override
         public String toString() {
-            return "CarAreaConfig{" +
-                    "mMinValue=" + mMinValue +
-                    ", mMaxValue=" + mMaxValue +
-                    '}';
+            return "CarAreaConfig{"
+                    + "mMinValue=" + mMinValue
+                    + ", mMaxValue=" + mMaxValue
+                    + '}';
         }
     }
 
@@ -629,6 +634,9 @@
             return this;
         }
 
+        /**
+         * Builds a new {@link CarPropertyConfig}.
+         */
         public CarPropertyConfig<T> build() {
             return new CarPropertyConfig<>(mAccess, mAreaType, mChangeMode, mConfigArray,
                                            mConfigString, mMaxSampleRate, mMinSampleRate,
diff --git a/car-lib/src/android/car/hardware/CarPropertyValue.java b/car-lib/src/android/car/hardware/CarPropertyValue.java
index 38af395..b946af9 100644
--- a/car-lib/src/android/car/hardware/CarPropertyValue.java
+++ b/car-lib/src/android/car/hardware/CarPropertyValue.java
@@ -26,6 +26,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 
 /**
  * Stores values broken down by area for a vehicle property.
@@ -35,7 +36,7 @@
  *
  */
 public final class CarPropertyValue<T> implements Parcelable {
-    private final static Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
 
     private final int mPropertyId;
     private final int mAreaId;
@@ -152,7 +153,7 @@
 
         // Special handling for String and byte[] to mitigate transaction buffer limitations.
         if (String.class.equals(valueClass)) {
-            dest.writeBlob(((String)mValue).getBytes(DEFAULT_CHARSET));
+            dest.writeBlob(((String) mValue).getBytes(DEFAULT_CHARSET));
         } else if (byte[].class.equals(valueClass)) {
             dest.writeBlob((byte[]) mValue);
         } else {
@@ -199,12 +200,12 @@
     /** @hide */
     @Override
     public String toString() {
-        return "CarPropertyValue{" +
-                "mPropertyId=0x" + toHexString(mPropertyId) +
-                ", mAreaId=0x" + toHexString(mAreaId) +
-                ", mStatus=" + mStatus +
-                ", mTimestamp=" + mTimestamp +
-                ", mValue=" + mValue +
-                '}';
+        return "CarPropertyValue{"
+                + "mPropertyId=0x" + toHexString(mPropertyId)
+                + ", mAreaId=0x" + toHexString(mAreaId)
+                + ", mStatus=" + mStatus
+                + ", mTimestamp=" + mTimestamp
+                + ", mValue=" + mValue
+                + '}';
     }
 }
diff --git a/car-lib/src/android/car/hardware/CarSensorConfig.java b/car-lib/src/android/car/hardware/CarSensorConfig.java
index 77f8dc5..4910def 100644
--- a/car-lib/src/android/car/hardware/CarSensorConfig.java
+++ b/car-lib/src/android/car/hardware/CarSensorConfig.java
@@ -19,7 +19,6 @@
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
-import java.util.ArrayList;
 
 /**
  * A CarSensorConfig object corresponds to a single sensor type coming from the car.
@@ -28,20 +27,20 @@
 public class CarSensorConfig implements Parcelable {
     /** List of property specific mapped elements in bundle for WHEEL_TICK_DISTANCE sensor*/
     /** @hide */
-    public final static String WHEEL_TICK_DISTANCE_SUPPORTED_WHEELS =
-        "android.car.wheelTickDistanceSupportedWheels";
+    public static final String WHEEL_TICK_DISTANCE_SUPPORTED_WHEELS =
+            "android.car.wheelTickDistanceSupportedWheels";
     /** @hide */
-    public final static String WHEEL_TICK_DISTANCE_FRONT_LEFT_UM_PER_TICK =
-        "android.car.wheelTickDistanceFrontLeftUmPerTick";
+    public static final String WHEEL_TICK_DISTANCE_FRONT_LEFT_UM_PER_TICK =
+            "android.car.wheelTickDistanceFrontLeftUmPerTick";
     /** @hide */
-    public final static String WHEEL_TICK_DISTANCE_FRONT_RIGHT_UM_PER_TICK =
-        "android.car.wheelTickDistanceFrontRightUmPerTick";
+    public static final String WHEEL_TICK_DISTANCE_FRONT_RIGHT_UM_PER_TICK =
+            "android.car.wheelTickDistanceFrontRightUmPerTick";
     /** @hide */
-    public final static String WHEEL_TICK_DISTANCE_REAR_RIGHT_UM_PER_TICK =
-        "android.car.wheelTickDistanceRearRightUmPerTick";
+    public static final String WHEEL_TICK_DISTANCE_REAR_RIGHT_UM_PER_TICK =
+            "android.car.wheelTickDistanceRearRightUmPerTick";
     /** @hide */
-    public final static String WHEEL_TICK_DISTANCE_REAR_LEFT_UM_PER_TICK =
-        "android.car.wheelTickDistanceRearLeftUmPerTick";
+    public static final String WHEEL_TICK_DISTANCE_REAR_LEFT_UM_PER_TICK =
+            "android.car.wheelTickDistanceRearLeftUmPerTick";
 
     /** Config data stored in Bundle */
     private final Bundle mConfig;
@@ -67,16 +66,19 @@
     }
 
     /** @hide */
-    public static final Parcelable.Creator<CarSensorConfig> CREATOR
-    = new Parcelable.Creator<CarSensorConfig>() {
-        public CarSensorConfig createFromParcel(Parcel in) {
-            return new CarSensorConfig(in);
-        }
+    public static final Parcelable.Creator<CarSensorConfig> CREATOR =
+            new Parcelable.Creator<CarSensorConfig>() {
 
-        public CarSensorConfig[] newArray(int size) {
-            return new CarSensorConfig[size];
-        }
-    };
+            @Override
+            public CarSensorConfig createFromParcel(Parcel in) {
+                return new CarSensorConfig(in);
+            }
+
+            @Override
+            public CarSensorConfig[] newArray(int size) {
+                return new CarSensorConfig[size];
+            }
+        };
 
     /** @hide */
     public CarSensorConfig(int type, Bundle b) {
@@ -101,10 +103,9 @@
     public int getInt(String key) {
         if (mConfig.containsKey(key)) {
             return mConfig.getInt(key);
-        } else {
-            throw new IllegalArgumentException("SensorType " + mType +
-                " does not contain key: " + key);
         }
+        throw new IllegalArgumentException("SensorType " + mType
+            + " does not contain key: " + key);
     }
 
     /** @hide */
diff --git a/car-lib/src/android/car/hardware/CarSensorEvent.java b/car-lib/src/android/car/hardware/CarSensorEvent.java
index b92a789..3f33a8d 100644
--- a/car-lib/src/android/car/hardware/CarSensorEvent.java
+++ b/car-lib/src/android/car/hardware/CarSensorEvent.java
@@ -164,8 +164,8 @@
         dest.writeLongArray(longValues);
     }
 
-    public static final Parcelable.Creator<CarSensorEvent> CREATOR
-    = new Parcelable.Creator<CarSensorEvent>() {
+    public static final Parcelable.Creator<CarSensorEvent> CREATOR =
+            new Parcelable.Creator<CarSensorEvent>() {
         public CarSensorEvent createFromParcel(Parcel in) {
             return new CarSensorEvent(in);
         }
@@ -203,6 +203,9 @@
                 "Invalid sensor type: expected %d, got %d", type, sensorType));
     }
 
+    /**
+     * Environment data with timestamp and temperature.
+     */
     public static class EnvironmentData {
         public long timestamp;
         /** If unsupported by the car, this value is NaN. */
@@ -577,8 +580,7 @@
      *     CarSensorEvent.
      * @hide
      */
-    public CarFuelDoorOpenData getCarFuelDoorOpenData(
-        CarFuelDoorOpenData data) {
+    public CarFuelDoorOpenData getCarFuelDoorOpenData(CarFuelDoorOpenData data) {
         checkType(CarSensorManager.SENSOR_TYPE_FUEL_DOOR_OPEN);
         if (data == null) {
             data = new CarFuelDoorOpenData();
@@ -608,8 +610,7 @@
      *     CarSensorEvent.
      * @hide
      */
-    public CarEvBatteryLevelData getCarEvBatteryLevelData(
-        CarEvBatteryLevelData data) {
+    public CarEvBatteryLevelData getCarEvBatteryLevelData(CarEvBatteryLevelData data) {
         checkType(CarSensorManager.SENSOR_TYPE_EV_BATTERY_LEVEL);
         if (data == null) {
             data = new CarEvBatteryLevelData();
@@ -646,8 +647,7 @@
      *     CarSensorEvent.
      * @hide
      */
-    public CarEvChargePortOpenData getCarEvChargePortOpenData(
-        CarEvChargePortOpenData data) {
+    public CarEvChargePortOpenData getCarEvChargePortOpenData(CarEvChargePortOpenData data) {
         checkType(CarSensorManager.SENSOR_TYPE_EV_CHARGE_PORT_OPEN);
         if (data == null) {
             data = new CarEvChargePortOpenData();
@@ -668,7 +668,7 @@
 
     /**
      * Convenience method for obtaining a {@link CarEvChargePortConnectedData} object from a
-     * CarSensorEvent object with type {@link CarSensorManager#SENSOR_TYPE_EV_CHARGE_PORT_CONNECTED}.
+     * CarSensorEvent with type {@link CarSensorManager#SENSOR_TYPE_EV_CHARGE_PORT_CONNECTED}.
      *
      * @param data an optional output parameter which, if non-null, will be used by this method
      *     instead of a newly created object.
@@ -677,7 +677,7 @@
      * @hide
      */
     public CarEvChargePortConnectedData getCarEvChargePortConnectedData(
-        CarEvChargePortConnectedData data) {
+            CarEvChargePortConnectedData data) {
         checkType(CarSensorManager.SENSOR_TYPE_EV_CHARGE_PORT_CONNECTED);
         if (data == null) {
             data = new CarEvChargePortConnectedData();
@@ -709,7 +709,7 @@
      * @hide
      */
     public CarEvBatteryChargeRateData getCarEvBatteryChargeRateData(
-        CarEvBatteryChargeRateData data) {
+            CarEvBatteryChargeRateData data) {
         checkType(CarSensorManager.SENSOR_TYPE_EV_BATTERY_CHARGE_RATE);
         if (data == null) {
             data = new CarEvBatteryChargeRateData();
diff --git a/car-lib/src/android/car/hardware/CarSensorManager.java b/car-lib/src/android/car/hardware/CarSensorManager.java
index 082c8eb..d047aa1 100644
--- a/car-lib/src/android/car/hardware/CarSensorManager.java
+++ b/car-lib/src/android/car/hardware/CarSensorManager.java
@@ -496,7 +496,7 @@
      * A CarSensorConfig object is returned for every sensor type.  However, if there is no
      * config, the data will be empty.
      *
-     * @param sensor type to request
+     * @param type sensor type to request
      * @return CarSensorConfig object
      * @hide
      */
diff --git a/car-lib/src/android/car/hardware/CarVendorExtensionManager.java b/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
index b796156..639eab1 100644
--- a/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
+++ b/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
@@ -45,8 +45,8 @@
 @SystemApi
 public final class CarVendorExtensionManager extends CarManagerBase {
 
-    private final static boolean DBG = false;
-    private final static String TAG = CarVendorExtensionManager.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final String TAG = CarVendorExtensionManager.class.getSimpleName();
     private final CarPropertyManager mPropertyManager;
 
     @GuardedBy("mLock")
diff --git a/car-lib/src/android/car/hardware/cabin/CarCabinManager.java b/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
index 7318176..7114ad8 100644
--- a/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
+++ b/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
@@ -57,8 +57,8 @@
 @Deprecated
 @SystemApi
 public final class CarCabinManager extends CarManagerBase {
-    private final static boolean DBG = false;
-    private final static String TAG = "CarCabinManager";
+    private static final boolean DBG = false;
+    private static final String TAG = CarCabinManager.class.getSimpleName();
     private final CarPropertyManager mCarPropertyMgr;
     private final ArraySet<CarCabinEventCallback> mCallbacks = new ArraySet<>();
     private CarPropertyEventListenerToBase mListenerToBase = null;
@@ -416,7 +416,7 @@
     private static class CarPropertyEventListenerToBase implements CarPropertyEventCallback {
         private final WeakReference<CarCabinManager> mManager;
 
-        public CarPropertyEventListenerToBase(CarCabinManager manager) {
+        CarPropertyEventListenerToBase(CarCabinManager manager) {
             mManager = new WeakReference<>(manager);
         }
 
diff --git a/car-lib/src/android/car/hardware/power/CarPowerManager.java b/car-lib/src/android/car/hardware/power/CarPowerManager.java
index d9ff33d..54d7b28 100644
--- a/car-lib/src/android/car/hardware/power/CarPowerManager.java
+++ b/car-lib/src/android/car/hardware/power/CarPowerManager.java
@@ -35,8 +35,8 @@
  */
 @SystemApi
 public class CarPowerManager extends CarManagerBase {
-    private final static boolean DBG = false;
-    private final static String TAG = "CarPowerManager";
+    private static final boolean DBG = false;
+    private static final String TAG = CarPowerManager.class.getSimpleName();
 
     private final Object mLock = new Object();
     private final ICarPower mService;
@@ -215,7 +215,7 @@
      * @hide
      */
     public void setListenerWithCompletion(CarPowerStateListenerWithCompletion listener) {
-        synchronized(mLock) {
+        synchronized (mLock) {
             if (mListener != null || mListenerWithCompletion != null) {
                 throw new IllegalStateException("Listener must be cleared first");
             }
diff --git a/car-lib/src/android/car/hardware/property/CarPropertyEvent.java b/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
index c423c2f..4d5498d 100644
--- a/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
+++ b/car-lib/src/android/car/hardware/property/CarPropertyEvent.java
@@ -42,12 +42,16 @@
     /**
      * @return EventType field
      */
-    public int getEventType() { return mEventType; }
+    public int getEventType() {
+        return mEventType;
+    }
 
     /**
      * Returns {@link CarPropertyValue} associated with this event.
      */
-    public CarPropertyValue<?> getCarPropertyValue() { return mCarPropertyValue; }
+    public CarPropertyValue<?> getCarPropertyValue() {
+        return mCarPropertyValue;
+    }
 
     @Override
     public int describeContents() {
@@ -61,16 +65,19 @@
         dest.writeParcelable(mCarPropertyValue, flags);
     }
 
-    public static final Parcelable.Creator<CarPropertyEvent> CREATOR
-            = new Parcelable.Creator<CarPropertyEvent>() {
+    public static final Parcelable.Creator<CarPropertyEvent> CREATOR =
+            new Parcelable.Creator<CarPropertyEvent>() {
+
+        @Override
         public CarPropertyEvent createFromParcel(Parcel in) {
             return new CarPropertyEvent(in);
         }
 
+        @Override
         public CarPropertyEvent[] newArray(int size) {
             return new CarPropertyEvent[size];
         }
-    };
+        };
 
     /**
      * Constructor for {@link CarPropertyEvent}.
diff --git a/car-lib/src/android/car/hardware/property/CarPropertyManager.java b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
index 5c5cebb..e45eaa8 100644
--- a/car-lib/src/android/car/hardware/property/CarPropertyManager.java
+++ b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
@@ -176,10 +176,11 @@
             for (CarPropertyConfig carPropertyConfig : configs) {
                 mConfigMap.put(carPropertyConfig.getPropertyId(), carPropertyConfig);
             }
-        } catch (Exception e) {
+        } catch (RemoteException e) {
             Log.e(TAG, "getPropertyList exception ", e);
             throw new RuntimeException(e);
         }
+
         Handler eventHandler = getEventHandler();
         if (eventHandler == null) {
             mHandler = null;
diff --git a/car-lib/src/android/car/navigation/CarNavigationStatusManager.java b/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
index 2aa2f10..e94a188 100644
--- a/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
+++ b/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
@@ -61,6 +61,9 @@
      * @param bundle object holding data about the navigation event. This information is
      *               generated using <a href="https://developer.android.com/reference/androidx/car/cluster/navigation/NavigationState.html#toParcelable()">
      *               androidx.car.cluster.navigation.NavigationState#toParcelable()</a>
+     *
+     * @throws IllegalStateException if the client is not holding
+     *                 {@link android.car.CarAppFocusManager#APP_FOCUS_TYPE_NAVIGATION} focus.
      */
     @RequiresPermission(Car.PERMISSION_CAR_NAVIGATION_MANAGER)
     public void sendNavigationStateChange(Bundle bundle) {
diff --git a/car-lib/src/android/car/settings/CarSettings.java b/car-lib/src/android/car/settings/CarSettings.java
index e9af8eb..ef78ff6 100644
--- a/car-lib/src/android/car/settings/CarSettings.java
+++ b/car-lib/src/android/car/settings/CarSettings.java
@@ -1,17 +1,17 @@
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
- *   Licensed under the Apache License, Version 2.0 (the "License");
- *   you may not use this file except in compliance with the License.
- *   You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
  *
- *        http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- *   Unless required by applicable law or agreed to in writing, software
- *   distributed under the License is distributed on an "AS IS" BASIS,
- *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *   See the License for the specific language governing permissions and
- *   limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.settings;
diff --git a/car-lib/src/android/car/user/CarUserManager.java b/car-lib/src/android/car/user/CarUserManager.java
index 784ee48..867186c 100644
--- a/car-lib/src/android/car/user/CarUserManager.java
+++ b/car-lib/src/android/car/user/CarUserManager.java
@@ -18,7 +18,6 @@
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.myUid;
 
 import android.annotation.CallbackExecutor;
@@ -32,7 +31,6 @@
 import android.car.Car;
 import android.car.CarManagerBase;
 import android.car.ICarUserService;
-import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -54,7 +52,9 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 /**
@@ -69,8 +69,7 @@
     private static final String TAG = CarUserManager.class.getSimpleName();
     private static final int HAL_TIMEOUT_MS = CarProperties.user_hal_timeout().orElse(5_000);
 
-    // TODO(b/144120654): STOPSHIP - set to false
-    private static final boolean DBG = true;
+    private static final boolean DBG = false;
 
     /**
      * {@link UserLifecycleEvent} called when the user is starting, for components to initialize
@@ -231,6 +230,8 @@
     /**
      * Adds a listener for {@link UserLifecycleEvent user lifecycle events}.
      *
+     * @throws IllegalStateException if the listener was already added.
+     *
      * @hide
      */
     @SystemApi
@@ -238,15 +239,13 @@
     @RequiresPermission(anyOf = {INTERACT_ACROSS_USERS, INTERACT_ACROSS_USERS_FULL})
     public void addListener(@NonNull @CallbackExecutor Executor executor,
             @NonNull UserLifecycleListener listener) {
-        checkInteractAcrossUsersPermission();
-
-        // TODO(b/144120654): add unit tests to validate input
-        // - executor cannot be null
-        // - listener cannot be null
-        // - listener must not be added before
+        Objects.requireNonNull(executor, "executor cannot be null");
+        Objects.requireNonNull(listener, "listener cannot be null");
 
         int uid = myUid();
         synchronized (mLock) {
+            Preconditions.checkState(mListeners == null || !mListeners.containsKey(listener),
+                    "already called for this listener");
             if (mReceiver == null) {
                 mReceiver = new LifecycleResultReceiver();
                 try {
@@ -271,24 +270,20 @@
     /**
      * Removes a listener for {@link UserLifecycleEvent user lifecycle events}.
      *
+     * @throws IllegalStateException if the listener was not added beforehand.
+     *
      * @hide
      */
     @SystemApi
     @TestApi
     @RequiresPermission(anyOf = {INTERACT_ACROSS_USERS, INTERACT_ACROSS_USERS_FULL})
     public void removeListener(@NonNull UserLifecycleListener listener) {
-        checkInteractAcrossUsersPermission();
+        Objects.requireNonNull(listener, "listener cannot be null");
 
-        // TODO(b/144120654): add unit tests to validate input
-        // - listener cannot be null
-        // - listener must not be added before
         int uid = myUid();
         synchronized (mLock) {
-            if (mListeners == null) {
-                Log.w(TAG, "removeListener(): no listeners for uid " + uid);
-                return;
-            }
-
+            Preconditions.checkState(mListeners != null && mListeners.containsKey(listener),
+                    "not called for this listener yet");
             mListeners.remove(listener);
 
             if (!mListeners.isEmpty()) {
@@ -318,14 +313,14 @@
      *
      * @hide
      */
-    @Nullable
+    @NonNull
     @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
-    public GetUserIdentificationAssociationResponse getUserIdentificationAssociation(
+    public UserIdentificationAssociationResponse getUserIdentificationAssociation(
             @NonNull int... types) {
         Preconditions.checkArgument(!ArrayUtils.isEmpty(types), "must have at least one type");
         EventLog.writeEvent(EventLogTags.CAR_USER_MGR_GET_USER_AUTH_REQ, types.length);
         try {
-            GetUserIdentificationAssociationResponse response =
+            UserIdentificationAssociationResponse response =
                     mService.getUserIdentificationAssociation(types);
             if (response != null) {
                 EventLog.writeEvent(EventLogTags.CAR_USER_MGR_GET_USER_AUTH_RESP,
@@ -338,6 +333,59 @@
     }
 
     /**
+     * Sets the user authentication types associated with this manager's user.
+     *
+     * @hide
+     */
+    @NonNull
+    public AndroidFuture<UserIdentificationAssociationResponse> setUserIdentificationAssociation(
+            @NonNull int[] types, @NonNull int[] values) {
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(types), "must have at least one type");
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(values), "must have at least one value");
+        if (types.length != values.length) {
+            throw new IllegalArgumentException("types (" + Arrays.toString(types) + ") and values ("
+                    + Arrays.toString(values) + ") should have the same length");
+        }
+        // TODO(b/153900032): move this logic to a common helper
+        Object[] loggedValues = new Integer[types.length * 2];
+        for (int i = 0; i < types.length; i++) {
+            loggedValues[i * 2] = types[i];
+            loggedValues[i * 2 + 1 ] = values[i];
+        }
+        EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_REQ, loggedValues);
+
+        try {
+            AndroidFuture<UserIdentificationAssociationResponse> future =
+                    new AndroidFuture<UserIdentificationAssociationResponse>() {
+                @Override
+                protected void onCompleted(UserIdentificationAssociationResponse result,
+                        Throwable err) {
+                    if (result != null) {
+                        int[] rawValues = result.getValues();
+                        // TODO(b/153900032): move this logic to a common helper
+                        Object[] loggedValues = new Object[rawValues.length];
+                        for (int i = 0; i < rawValues.length; i++) {
+                            loggedValues[i] = rawValues[i];
+                        }
+                        EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_RESP,
+                                loggedValues);
+                    } else {
+                        Log.w(TAG, "setUserIdentificationAssociation(" + Arrays.toString(types)
+                                + ", " + Arrays.toString(values) + ") failed: " + err);
+                    }
+                    super.onCompleted(result, err);
+                };
+            };
+            mService.setUserIdentificationAssociation(HAL_TIMEOUT_MS, types, values, future);
+            return future;
+        } catch (RemoteException e) {
+            AndroidFuture<UserIdentificationAssociationResponse> future = new AndroidFuture<>();
+            future.complete(UserIdentificationAssociationResponse.forFailure());
+            return handleRemoteExceptionFromCarService(e, future);
+        }
+    }
+
+    /**
      * Sets a callback to be notified before user switch. It should only be used by Car System UI.
      *
      * @hide
@@ -431,20 +479,6 @@
         }
     }
 
-    private void checkInteractAcrossUsersPermission() {
-        checkInteractAcrossUsersPermission(getContext());
-    }
-
-    private static void checkInteractAcrossUsersPermission(Context context) {
-        if (context.checkSelfPermission(INTERACT_ACROSS_USERS) != PERMISSION_GRANTED
-                && context.checkSelfPermission(INTERACT_ACROSS_USERS_FULL) != PERMISSION_GRANTED) {
-            throw new SecurityException(
-                    "Must have either " + android.Manifest.permission.INTERACT_ACROSS_USERS + " or "
-                            + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
-                            + " permission");
-        }
-    }
-
     // NOTE: this method is called by ExperimentalCarUserManager, so it can get the mService.
     // "Real" ExperimentalCarUserManager instances should be obtained through
     //    ExperimentalCarUserManager.from(mCarUserManager)
diff --git a/car-lib/src/android/car/user/ExperimentalCarUserManager.java b/car-lib/src/android/car/user/ExperimentalCarUserManager.java
index e5a2011..1396971 100644
--- a/car-lib/src/android/car/user/ExperimentalCarUserManager.java
+++ b/car-lib/src/android/car/user/ExperimentalCarUserManager.java
@@ -27,6 +27,9 @@
 import android.content.pm.UserInfo;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.infra.AndroidFuture;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -42,6 +45,8 @@
 @ExperimentalFeature
 public final class ExperimentalCarUserManager extends CarManagerBase {
 
+    private static final String TAG = ExperimentalCarUserManager.class.getSimpleName();
+
     /**
      *  User id representing invalid user.
      */
@@ -108,16 +113,31 @@
      * Switches a driver to the given user.
      *
      * @param driverId User id of the driver to switch to.
-     * @return {@code true} if user switching succeeds, or {@code false} if it fails.
+     * @return an {@link AndroidFuture} that can be used to track operation's completion and
+     *         retrieve its result (if any).
      *
      * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
-    public boolean switchDriver(@UserIdInt int driverId) {
+    public AndroidFuture<UserSwitchResult> switchDriver(@UserIdInt int driverId) {
         try {
-            return mService.switchDriver(driverId);
+            AndroidFuture<UserSwitchResult> future = new AndroidFuture<>() {
+                @Override
+                protected void onCompleted(UserSwitchResult result, Throwable err) {
+                    if (result == null) {
+                        Log.w(TAG, "switchDriver(" + driverId + ") failed: " + err);
+                    }
+                    super.onCompleted(result, err);
+                }
+            };
+            mService.switchDriver(driverId, future);
+            return future;
         } catch (RemoteException e) {
-            return handleRemoteExceptionFromCarService(e, false);
+            AndroidFuture<UserSwitchResult> future = new AndroidFuture<>();
+            future.complete(
+                    new UserSwitchResult(UserSwitchResult.STATUS_HAL_INTERNAL_FAILURE, null));
+            handleRemoteExceptionFromCarService(e);
+            return future;
         }
     }
 
diff --git a/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java b/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java
deleted file mode 100644
index 12bee07..0000000
--- a/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java
+++ /dev/null
@@ -1,181 +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.
- */
-
-package android.car.user;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.os.Parcelable;
-
-import com.android.internal.util.DataClass;
-
-/**
- * Results of a {@link CarUserManager#getUserIdentificationAssociation(int[]) request.
- *
- * @hide
- */
-@DataClass(
-        genToString = true,
-        genHiddenConstructor = true,
-        genHiddenConstDefs = true)
-public final class GetUserIdentificationAssociationResponse implements Parcelable {
-
-    /**
-     * Gets the error message returned by the HAL.
-     */
-    @Nullable
-    private String mErrorMessage;
-
-    /**
-     * Gets the list of
-     * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}
-     * associates with the request.
-     */
-    @NonNull
-    private final int[] mValues;
-
-
-
-
-    // Code below generated by codegen v1.0.15.
-    //
-    // DO NOT MODIFY!
-    // CHECKSTYLE:OFF Generated code
-    //
-    // To regenerate run:
-    // $ codegen $ANDROID_BUILD_TOP/packages/services/Car/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java
-    //
-    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
-    //   Settings > Editor > Code Style > Formatter Control
-    //@formatter:off
-
-
-    /**
-     * Creates a new GetUserIdentificationAssociationResponse.
-     *
-     * @param errorMessage
-     *   Gets the error message returned by the HAL.
-     * @param values
-     *   Gets the list of
-     *   {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}
-     *   associates with the request.
-     * @hide
-     */
-    @DataClass.Generated.Member
-    public GetUserIdentificationAssociationResponse(
-            @Nullable String errorMessage,
-            @NonNull int[] values) {
-        this.mErrorMessage = errorMessage;
-        this.mValues = values;
-        com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mValues);
-
-        // onConstructed(); // You can define this method to get a callback
-    }
-
-    /**
-     * Gets the error message returned by the HAL.
-     */
-    @DataClass.Generated.Member
-    public @Nullable String getErrorMessage() {
-        return mErrorMessage;
-    }
-
-    /**
-     * Gets the list of
-     * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}
-     * associates with the request.
-     */
-    @DataClass.Generated.Member
-    public @NonNull int[] getValues() {
-        return mValues;
-    }
-
-    @Override
-    @DataClass.Generated.Member
-    public String toString() {
-        // You can override field toString logic by defining methods like:
-        // String fieldNameToString() { ... }
-
-        return "GetUserIdentificationAssociationResponse { " +
-                "errorMessage = " + mErrorMessage + ", " +
-                "values = " + java.util.Arrays.toString(mValues) +
-        " }";
-    }
-
-    @Override
-    @DataClass.Generated.Member
-    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
-        // You can override field parcelling by defining methods like:
-        // void parcelFieldName(Parcel dest, int flags) { ... }
-
-        byte flg = 0;
-        if (mErrorMessage != null) flg |= 0x1;
-        dest.writeByte(flg);
-        if (mErrorMessage != null) dest.writeString(mErrorMessage);
-        dest.writeIntArray(mValues);
-    }
-
-    @Override
-    @DataClass.Generated.Member
-    public int describeContents() { return 0; }
-
-    /** @hide */
-    @SuppressWarnings({"unchecked", "RedundantCast"})
-    @DataClass.Generated.Member
-    /* package-private */ GetUserIdentificationAssociationResponse(@NonNull android.os.Parcel in) {
-        // You can override field unparcelling by defining methods like:
-        // static FieldType unparcelFieldName(Parcel in) { ... }
-
-        byte flg = in.readByte();
-        String errorMessage = (flg & 0x1) == 0 ? null : in.readString();
-        int[] values = in.createIntArray();
-
-        this.mErrorMessage = errorMessage;
-        this.mValues = values;
-        com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mValues);
-
-        // onConstructed(); // You can define this method to get a callback
-    }
-
-    @DataClass.Generated.Member
-    public static final @NonNull Parcelable.Creator<GetUserIdentificationAssociationResponse> CREATOR
-            = new Parcelable.Creator<GetUserIdentificationAssociationResponse>() {
-        @Override
-        public GetUserIdentificationAssociationResponse[] newArray(int size) {
-            return new GetUserIdentificationAssociationResponse[size];
-        }
-
-        @Override
-        public GetUserIdentificationAssociationResponse createFromParcel(@NonNull android.os.Parcel in) {
-            return new GetUserIdentificationAssociationResponse(in);
-        }
-    };
-
-    @DataClass.Generated(
-            time = 1587769987549L,
-            codegenVersion = "1.0.15",
-            sourceFile = "packages/services/Car/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java",
-            inputSignatures = "private @android.annotation.Nullable java.lang.String mErrorMessage\nprivate final @android.annotation.NonNull int[] mValues\nclass GetUserIdentificationAssociationResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
-    @Deprecated
-    private void __metadata() {}
-
-
-    //@formatter:on
-    // End of generated code
-
-}
diff --git a/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.aidl b/car-lib/src/android/car/user/UserIdentificationAssociationResponse.aidl
similarity index 92%
rename from car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.aidl
rename to car-lib/src/android/car/user/UserIdentificationAssociationResponse.aidl
index d8ac0dc..9e292d4 100644
--- a/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.aidl
+++ b/car-lib/src/android/car/user/UserIdentificationAssociationResponse.aidl
@@ -16,4 +16,4 @@
 
 package android.car.user;
 
-parcelable GetUserIdentificationAssociationResponse;
+parcelable UserIdentificationAssociationResponse;
diff --git a/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java b/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java
new file mode 100644
index 0000000..d2cdb44
--- /dev/null
+++ b/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java
@@ -0,0 +1,244 @@
+/*
+ * 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.
+ */
+
+package android.car.user;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcelable;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.DataClass;
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Results of a {@link CarUserManager#getUserIdentificationAssociation(int[]) request.
+ *
+ * @hide
+ */
+@DataClass(
+        genToString = true,
+        genHiddenConstructor = false,
+        genHiddenConstDefs = true)
+public final class UserIdentificationAssociationResponse implements Parcelable {
+
+    /**
+     * Whether the request was successful.
+     *
+     * <p>A successful option has non-null {@link #getValues()}
+     */
+    private final boolean mSuccess;
+
+    /**
+     * Gets the error message returned by the HAL.
+     */
+    @Nullable
+    private final String mErrorMessage;
+
+    /**
+     * Gets the list of values associated with the request.
+     *
+     * <p><b>NOTE: </b>It's only set when the response is {@link #isSuccess() successful}.
+     *
+     * <p>For {@link CarUserManager#getUserIdentificationAssociation(int...)}, the values are
+     * defined on
+     * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}.
+     *
+     * <p>For {@link CarUserManager#setUserIdentificationAssociation(int...)}, the values are
+     * defined on
+     * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue}.
+     */
+    @Nullable
+    private final int[] mValues;
+
+    private UserIdentificationAssociationResponse(
+            boolean success,
+            @Nullable String errorMessage,
+            @Nullable int[] values) {
+        this.mSuccess = success;
+        this.mErrorMessage = errorMessage;
+        this.mValues = values;
+    }
+
+    /**
+     * Factory method for failed UserIdentificationAssociationResponse requests.
+     */
+    @NonNull
+    public static UserIdentificationAssociationResponse forFailure() {
+        return forFailure(/* errorMessage= */ null);
+    }
+
+    /**
+     * Factory method for failed UserIdentificationAssociationResponse requests.
+     */
+    @NonNull
+    public static UserIdentificationAssociationResponse forFailure(@Nullable String errorMessage) {
+        return new UserIdentificationAssociationResponse(/* success= */ false,
+                errorMessage, /* values= */ null);
+    }
+
+    /**
+     * Factory method for successful UserIdentificationAssociationResponse requests.
+     */
+    @NonNull
+    public static UserIdentificationAssociationResponse forSuccess(@NonNull int[] values) {
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(values), "must have at least one value");
+        return new UserIdentificationAssociationResponse(/* success= */ true,
+                /* errorMessage= */ null, Objects.requireNonNull(values));
+    }
+
+    /**
+     * Factory method for successful UserIdentificationAssociationResponse requests.
+     */
+    @NonNull
+    public static UserIdentificationAssociationResponse forSuccess(@NonNull int[] values,
+            @Nullable String errorMessage) {
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(values), "must have at least one value");
+        return new UserIdentificationAssociationResponse(/* success= */ true,
+                errorMessage, Objects.requireNonNull(values));
+    }
+
+
+
+    // Code below generated by codegen v1.0.15.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen $ANDROID_BUILD_TOP/packages/services/Car/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    //@formatter:off
+
+
+    /**
+     * Whether the request was successful.
+     *
+     * <p>A successful option has non-null {@link #getValues()}
+     */
+    @DataClass.Generated.Member
+    public boolean isSuccess() {
+        return mSuccess;
+    }
+
+    /**
+     * Gets the error message returned by the HAL.
+     */
+    @DataClass.Generated.Member
+    public @Nullable String getErrorMessage() {
+        return mErrorMessage;
+    }
+
+    /**
+     * Gets the list of values associated with the request.
+     *
+     * <p><b>NOTE: </b>It's only set when the response is {@link #isSuccess() successful}.
+     *
+     * <p>For {@link CarUserManager#getUserIdentificationAssociation(int...)}, the values are
+     * defined on
+     * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}.
+     *
+     * <p>For {@link CarUserManager#setUserIdentificationAssociation(int...)}, the values are
+     * defined on
+     * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue}.
+     */
+    @DataClass.Generated.Member
+    public @Nullable int[] getValues() {
+        return mValues;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public String toString() {
+        // You can override field toString logic by defining methods like:
+        // String fieldNameToString() { ... }
+
+        return "UserIdentificationAssociationResponse { " +
+                "success = " + mSuccess + ", " +
+                "errorMessage = " + mErrorMessage + ", " +
+                "values = " + java.util.Arrays.toString(mValues) +
+        " }";
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        byte flg = 0;
+        if (mSuccess) flg |= 0x1;
+        if (mErrorMessage != null) flg |= 0x2;
+        if (mValues != null) flg |= 0x4;
+        dest.writeByte(flg);
+        if (mErrorMessage != null) dest.writeString(mErrorMessage);
+        if (mValues != null) dest.writeIntArray(mValues);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() { return 0; }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    /* package-private */ UserIdentificationAssociationResponse(@NonNull android.os.Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        byte flg = in.readByte();
+        boolean success = (flg & 0x1) != 0;
+        String errorMessage = (flg & 0x2) == 0 ? null : in.readString();
+        int[] values = (flg & 0x4) == 0 ? null : in.createIntArray();
+
+        this.mSuccess = success;
+        this.mErrorMessage = errorMessage;
+        this.mValues = values;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<UserIdentificationAssociationResponse> CREATOR
+            = new Parcelable.Creator<UserIdentificationAssociationResponse>() {
+        @Override
+        public UserIdentificationAssociationResponse[] newArray(int size) {
+            return new UserIdentificationAssociationResponse[size];
+        }
+
+        @Override
+        public UserIdentificationAssociationResponse createFromParcel(@NonNull android.os.Parcel in) {
+            return new UserIdentificationAssociationResponse(in);
+        }
+    };
+
+    @DataClass.Generated(
+            time = 1588982917194L,
+            codegenVersion = "1.0.15",
+            sourceFile = "packages/services/Car/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java",
+            inputSignatures = "private final  boolean mSuccess\nprivate final @android.annotation.Nullable java.lang.String mErrorMessage\nprivate final @android.annotation.Nullable int[] mValues\npublic static @android.annotation.NonNull android.car.user.UserIdentificationAssociationResponse forFailure()\npublic static @android.annotation.NonNull android.car.user.UserIdentificationAssociationResponse forFailure(java.lang.String)\npublic static @android.annotation.NonNull android.car.user.UserIdentificationAssociationResponse forSuccess(int[])\npublic static @android.annotation.NonNull android.car.user.UserIdentificationAssociationResponse forSuccess(int[],java.lang.String)\nclass UserIdentificationAssociationResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=false, genHiddenConstDefs=true)")
+    @Deprecated
+    private void __metadata() {}
+
+
+    //@formatter:on
+    // End of generated code
+
+}
diff --git a/car-lib/src/android/car/user/UserSwitchResult.java b/car-lib/src/android/car/user/UserSwitchResult.java
index 5d3e50f..5792bf6 100644
--- a/car-lib/src/android/car/user/UserSwitchResult.java
+++ b/car-lib/src/android/car/user/UserSwitchResult.java
@@ -42,6 +42,7 @@
      * @hide
      */
     public static final int STATUS_SUCCESSFUL = 1;
+
     /**
      * {@link UserSwitchStatus} called when user switch is only successful for Hal but not for
      * Android. Hal user switch rollover message have been sent.
@@ -49,6 +50,7 @@
      * @hide
      */
     public static final int STATUS_ANDROID_FAILURE = 2;
+
     /**
      * {@link UserSwitchStatus} called when user switch is failed for HAL. User switch for Android
      * is not called.
@@ -56,6 +58,7 @@
      * @hide
      */
     public static final int STATUS_HAL_FAILURE = 3;
+
     /**
      * {@link UserSwitchStatus} called when user switch is failed for HAL for some internal error.
      * User switch for Android is not called.
@@ -63,12 +66,14 @@
      * @hide
      */
     public static final int STATUS_HAL_INTERNAL_FAILURE = 4;
+
     /**
      * {@link UserSwitchStatus} called when target user is same as current user.
      *
      * @hide
      */
     public static final int STATUS_ALREADY_REQUESTED_USER = 5;
+
     /**
      * {@link UserSwitchStatus} called when another user switch request for the same target user is
      * in process.
@@ -86,6 +91,14 @@
     public static final int STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST = 7;
 
     /**
+     * {@link UserSwitchStatus} called when given parameters or environment states are invalid for
+     * switching user. HAL or Android user switch is not requested.
+     *
+     * @hide
+     */
+    public static final int STATUS_INVALID_REQUEST = 8;
+
+    /**
      * Gets the user switch result status.
      *
      * @return either {@link UserSwitchResult#STATUS_SUCCESSFUL},
@@ -93,8 +106,9 @@
      *         {@link UserSwitchResult#STATUS_HAL_FAILURE},
      *         {@link UserSwitchResult#STATUS_HAL_INTERNAL_FAILURE},
      *         {@link UserSwitchResult#STATUS_ALREADY_REQUESTED_USER},
-     *         {@link UserSwitchResult#STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO}, or
-     *         {@link UserSwitchResult#STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST}.
+     *         {@link UserSwitchResult#STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO}
+     *         {@link UserSwitchResult#STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST}, or
+     *         {@link UserSwitchResult#STATUS_INVALID_REQUEST}.
      */
     private final int mStatus;
 
@@ -105,6 +119,7 @@
     private final String mErrorMessage;
 
 
+
     // Code below generated by codegen v1.0.15.
     //
     // DO NOT MODIFY!
@@ -126,7 +141,8 @@
         STATUS_HAL_INTERNAL_FAILURE,
         STATUS_ALREADY_REQUESTED_USER,
         STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO,
-        STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST
+        STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST,
+        STATUS_INVALID_REQUEST
     })
     @Retention(RetentionPolicy.SOURCE)
     @DataClass.Generated.Member
@@ -150,6 +166,8 @@
                     return "STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO";
             case STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST:
                     return "STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST";
+            case STATUS_INVALID_REQUEST:
+                    return "STATUS_INVALID_REQUEST";
             default: return Integer.toHexString(value);
         }
     }
@@ -165,8 +183,9 @@
      *           {@link UserSwitchResult#STATUS_HAL_FAILURE},
      *           {@link UserSwitchResult#STATUS_HAL_INTERNAL_FAILURE},
      *           {@link UserSwitchResult#STATUS_ALREADY_REQUESTED_USER},
-     *           {@link UserSwitchResult#STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO}, or
-     *           {@link UserSwitchResult#STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST}.
+     *           {@link UserSwitchResult#STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO}
+     *           {@link UserSwitchResult#STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST}, or
+     *           {@link UserSwitchResult#STATUS_INVALID_REQUEST}.
      * @param errorMessage
      *   Gets the error message, if any.
      * @hide
@@ -189,8 +208,9 @@
      *         {@link UserSwitchResult#STATUS_HAL_FAILURE},
      *         {@link UserSwitchResult#STATUS_HAL_INTERNAL_FAILURE},
      *         {@link UserSwitchResult#STATUS_ALREADY_REQUESTED_USER},
-     *         {@link UserSwitchResult#STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO}, or
-     *         {@link UserSwitchResult#STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST}.
+     *         {@link UserSwitchResult#STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO}
+     *         {@link UserSwitchResult#STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST}, or
+     *         {@link UserSwitchResult#STATUS_INVALID_REQUEST}.
      */
     @DataClass.Generated.Member
     public int getStatus() {
@@ -266,10 +286,10 @@
     };
 
     @DataClass.Generated(
-            time = 1587768100440L,
+            time = 1589580732431L,
             codegenVersion = "1.0.15",
             sourceFile = "packages/services/Car/car-lib/src/android/car/user/UserSwitchResult.java",
-            inputSignatures = "public static final  int STATUS_SUCCESSFUL\npublic static final  int STATUS_ANDROID_FAILURE\npublic static final  int STATUS_HAL_FAILURE\npublic static final  int STATUS_HAL_INTERNAL_FAILURE\npublic static final  int STATUS_ALREADY_REQUESTED_USER\npublic static final  int STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO\npublic static final  int STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST\nprivate final  int mStatus\nprivate final @android.annotation.Nullable java.lang.String mErrorMessage\nclass UserSwitchResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
+            inputSignatures = "public static final  int STATUS_SUCCESSFUL\npublic static final  int STATUS_ANDROID_FAILURE\npublic static final  int STATUS_HAL_FAILURE\npublic static final  int STATUS_HAL_INTERNAL_FAILURE\npublic static final  int STATUS_ALREADY_REQUESTED_USER\npublic static final  int STATUS_TARGET_USER_ALREADY_BEING_SWITCHED_TO\npublic static final  int STATUS_TARGET_USER_ABANDONED_DUE_TO_A_NEW_REQUEST\npublic static final  int STATUS_INVALID_REQUEST\nprivate final  int mStatus\nprivate final @android.annotation.Nullable java.lang.String mErrorMessage\nclass UserSwitchResult extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/car-lib/src/android/car/vms/VmsAssociatedLayer.java b/car-lib/src/android/car/vms/VmsAssociatedLayer.java
index d34aa5d..7443857 100644
--- a/car-lib/src/android/car/vms/VmsAssociatedLayer.java
+++ b/car-lib/src/android/car/vms/VmsAssociatedLayer.java
@@ -24,7 +24,9 @@
 
 import com.android.internal.util.DataClass;
 
-import java.util.*;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
 
 /**
  * A Vehicle Map Service layer with a list of publisher IDs it is associated with.
diff --git a/car-lib/src/android/car/vms/VmsOperationRecorder.java b/car-lib/src/android/car/vms/VmsOperationRecorder.java
index 81265d2..f56c475 100644
--- a/car-lib/src/android/car/vms/VmsOperationRecorder.java
+++ b/car-lib/src/android/car/vms/VmsOperationRecorder.java
@@ -1,3 +1,19 @@
+/*
+ * 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 android.car.vms;
 
 import android.annotation.SystemApi;
@@ -224,6 +240,7 @@
             return Log.isLoggable(TAG, LEVEL);
         }
 
+        /** Logs the message passed as parameter. */
         public void write(String msg) {
             Log.println(LEVEL, TAG, msg);
         }
diff --git a/car-lib/src/android/car/watchdog/CarWatchdogManager.java b/car-lib/src/android/car/watchdog/CarWatchdogManager.java
index fff1283..d8fb7d9 100644
--- a/car-lib/src/android/car/watchdog/CarWatchdogManager.java
+++ b/car-lib/src/android/car/watchdog/CarWatchdogManager.java
@@ -51,7 +51,7 @@
 public final class CarWatchdogManager extends CarManagerBase {
 
     private static final String TAG = CarWatchdogManager.class.getSimpleName();
-    private static final boolean DEBUG = true; // STOPSHIP if true (b/151474489)
+    private static final boolean DEBUG = false; // STOPSHIP if true
     private static final int INVALID_SESSION_ID = -1;
     private static final int NUMBER_OF_CONDITIONS_TO_BE_MET = 2;
     // Message ID representing main thread activeness checking.
diff --git a/car-lib/src/com/android/car/internal/FeatureUtil.java b/car-lib/src/com/android/car/internal/FeatureUtil.java
index ad33eb9..c601a9d 100644
--- a/car-lib/src/com/android/car/internal/FeatureUtil.java
+++ b/car-lib/src/com/android/car/internal/FeatureUtil.java
@@ -21,6 +21,9 @@
  */
 public class FeatureUtil {
 
+    /**
+     * Ensures the flag passes as parameter is enabled.
+     */
     public static void assertFeature(boolean featureFlag) {
         if (!featureFlag) {
             throw new IllegalStateException("Feature not enabled");
diff --git a/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java b/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
index e5c5740..35f0274 100644
--- a/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
+++ b/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
@@ -31,10 +31,12 @@
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
+import android.os.Trace;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.util.Log;
 import android.util.Slog;
+import android.util.TimingsTraceLog;
 
 import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
 import com.android.internal.util.Preconditions;
@@ -81,6 +83,8 @@
  */
 public abstract class AbstractExtendedMockitoTestCase {
 
+    private static final boolean TRACE = false;
+
     private static final boolean VERBOSE = false;
     private static final String TAG = AbstractExtendedMockitoTestCase.class.getSimpleName();
 
@@ -92,23 +96,46 @@
     private MockitoSession mSession;
     private MockSettings mSettings;
 
+    @Nullable
+    private final TimingsTraceLog mTracer;
+
     @Rule
     public final WtfCheckerRule mWtfCheckerRule = new WtfCheckerRule();
 
+    protected AbstractExtendedMockitoTestCase() {
+        mTracer = TRACE ? new TimingsTraceLog(TAG, Trace.TRACE_TAG_APP) : null;
+    }
+
     @Before
     public final void startSession() {
-        if (VERBOSE) Log.v(TAG, getLogPrefix() + "startSession()");
+        beginTrace("startSession()");
+
+        beginTrace("startMocking()");
         mSession = newSessionBuilder().startMocking();
+        endTrace();
+
+        beginTrace("MockSettings()");
         mSettings = new MockSettings();
+        endTrace();
+
+        beginTrace("interceptWtfCalls()");
         interceptWtfCalls();
+        endTrace();
+
+        endTrace(); // startSession
     }
 
     @After
     public final void finishSession() {
-        if (VERBOSE) Log.v(TAG, getLogPrefix() + "finishSession()");
+        beginTrace("finishSession()");
         if (mSession != null) {
+            beginTrace("finishMocking()");
             mSession.finishMocking();
+            endTrace();
+        } else {
+            Log.w(TAG, getClass().getSimpleName() + ".finishSession(): no session");
         }
+        endTrace();
     }
 
     /**
@@ -172,7 +199,10 @@
     protected final void mockGetCurrentUser(@UserIdInt int userId) {
         if (VERBOSE) Log.v(TAG, getLogPrefix() + "mockGetCurrentUser(" + userId + ")");
         assertSpied(ActivityManager.class);
+
+        beginTrace("mockAmGetCurrentUser-" + userId);
         AndroidMockitoHelper.mockAmGetCurrentUser(userId);
+        endTrace();
     }
 
     /**
@@ -186,10 +216,40 @@
     protected final void mockIsHeadlessSystemUserMode(boolean mode) {
         if (VERBOSE) Log.v(TAG, getLogPrefix() + "mockIsHeadlessSystemUserMode(" + mode + ")");
         assertSpied(UserManager.class);
+
+        beginTrace("mockUmIsHeadlessSystemUserMode");
         AndroidMockitoHelper.mockUmIsHeadlessSystemUserMode(mode);
+        endTrace();
     }
 
-    protected void interceptWtfCalls() {
+    /**
+     * Starts a tracing message.
+     *
+     * <p>MUST be followed by a {@link #endTrace()} calls.
+     *
+     * <p>Ignored if {@value #VERBOSE} is {@code false}.
+     */
+    protected final void beginTrace(@NonNull String message) {
+        if (mTracer == null) return;
+
+        Log.d(TAG, getLogPrefix() + message);
+        mTracer.traceBegin(message);
+    }
+
+    /**
+     * Ends a tracing call.
+     *
+     * <p>MUST be called after {@link #beginTrace(String)}.
+     *
+     * <p>Ignored if {@value #VERBOSE} is {@code false}.
+     */
+    protected final void endTrace() {
+        if (mTracer == null) return;
+
+        mTracer.traceEnd();
+    }
+
+    private void interceptWtfCalls() {
         doAnswer((invocation) -> {
             return addWtf(invocation);
         }).when(() -> Log.wtf(anyString(), anyString()));
@@ -245,14 +305,23 @@
                     .spyStatic(Slog.class);
 
         onSessionBuilder(customBuilder);
+
+        if (VERBOSE) Log.v(TAG, "spied classes" + customBuilder.mStaticSpiedClasses);
+
         return builder.initMocks(this);
     }
 
-    private String getLogPrefix() {
+    /**
+     * Gets a prefix for {@link Log} calls
+     */
+    protected String getLogPrefix() {
         return getClass().getSimpleName() + ".";
     }
 
-    private void assertSpied(Class<?> clazz) {
+    /**
+     * Asserts the given class is being spied in the Mockito session.
+     */
+    protected void assertSpied(Class<?> clazz) {
         Preconditions.checkArgument(mStaticSpiedClasses.contains(clazz),
                 "did not call spyStatic() on %s", clazz.getName());
     }
@@ -295,20 +364,26 @@
                 @Override
                 public void evaluate() throws Throwable {
                     String testName = description.getMethodName();
-
                     if (VERBOSE) Log.v(TAG, "running " + testName);
+                    beginTrace("evaluate-" + testName);
                     base.evaluate();
+                    endTrace();
 
                     Method testMethod = AbstractExtendedMockitoTestCase.this.getClass()
                             .getMethod(testName);
                     ExpectWtf expectWtfAnnotation = testMethod.getAnnotation(ExpectWtf.class);
 
-                    if (expectWtfAnnotation != null) {
-                        if (VERBOSE) Log.v(TAG, "expecting wtf()");
-                        verifyWtfLogged();
-                    } else {
-                        if (VERBOSE) Log.v(TAG, "NOT expecting wtf()");
-                        verifyWtfNeverLogged();
+                    beginTrace("verify-wtfs");
+                    try {
+                        if (expectWtfAnnotation != null) {
+                            if (VERBOSE) Log.v(TAG, "expecting wtf()");
+                            verifyWtfLogged();
+                        } else {
+                            if (VERBOSE) Log.v(TAG, "NOT expecting wtf()");
+                            verifyWtfNeverLogged();
+                        }
+                    } finally {
+                        endTrace();
                     }
                 }
             };
@@ -351,6 +426,9 @@
 
             when(Settings.System.getIntForUser(any(), any(), anyInt(), anyInt()))
                     .thenAnswer(getIntAnswer);
+
+            when(Settings.System.putStringForUser(any(), any(), anyString(), anyInt()))
+                    .thenAnswer(insertObjectAnswer);
         }
 
         private Object insertObjectFromInvocation(InvocationOnMock invocation,
@@ -378,22 +456,27 @@
 
         @Nullable
         private <T> T get(String key, T defaultValue, Class<T> clazz) {
-            if (VERBOSE) Log.v(TAG, "Getting Setting " + key);
+            if (VERBOSE) {
+                Log.v(TAG, "get(): key=" + key + ", default=" + defaultValue + ", class=" + clazz);
+            }
             Object value = mSettingsMapping.get(key);
             if (value == null) {
+                if (VERBOSE) Log.v(TAG, "not found");
                 return defaultValue;
             }
+
+            if (VERBOSE) Log.v(TAG, "returning " + value);
             return safeCast(value, clazz);
         }
 
-        private <T> T safeCast(Object value, Class<T> clazz) {
+        private static <T> T safeCast(Object value, Class<T> clazz) {
             if (value == null) {
                 return null;
             }
             Preconditions.checkArgument(value.getClass() == clazz,
                     "Setting value has class %s but requires class %s",
                     value.getClass(), clazz);
-            return (T) value;
+            return clazz.cast(value);
         }
 
         private String getString(String key) {
@@ -401,7 +484,7 @@
         }
 
         public int getInt(String key) {
-            return (int) get(key, null, Integer.class);
+            return get(key, null, Integer.class);
         }
     }
 
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 4a26391..d05b013 100644
--- a/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java
+++ b/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java
@@ -24,16 +24,24 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.car.test.util.UserTestingHelper;
+import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
 import android.os.IBinder;
 import android.os.IInterface;
 import android.os.ServiceManager;
+import android.os.UserHandle;
 import android.os.UserManager;
 
+import com.android.internal.infra.AndroidFuture;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 /**
@@ -41,6 +49,8 @@
  */
 public final class AndroidMockitoHelper {
 
+    private static final long ASYNC_TIMEOUT_MS = 500;
+
     /**
      * Mocks a call to {@link ActivityManager#getCurrentUser()}.
      *
@@ -75,10 +85,27 @@
     public static UserInfo mockUmGetUserInfo(@NonNull UserManager um, @UserIdInt int userId,
             @UserInfoFlag int flags) {
         Objects.requireNonNull(um);
-        UserInfo userInfo = new UserTestingHelper.UserInfoBuilder(userId).setFlags(flags).build();
-        when(um.getUserInfo(userId)).thenReturn(userInfo);
+        UserInfo user = new UserTestingHelper.UserInfoBuilder(userId).setFlags(flags).build();
+        mockUmGetUserInfo(um, user);
+        return user;
+    }
 
-        return userInfo;
+    /**
+     * Mocks {@code UserManager.getUserInfo(userId)} to return the given {@link UserInfo}.
+     */
+    @NonNull
+    public static void mockUmGetUserInfo(@NonNull UserManager um, @NonNull UserInfo user) {
+        when(um.getUserInfo(user.id)).thenReturn(user);
+    }
+
+    /**
+     * Mocks {@code UserManager.getUserInfo(userId)} when the {@code userId} is the system user's.
+     */
+    @NonNull
+    public static void mockUmGetSystemUser(@NonNull UserManager um) {
+        UserInfo user = new UserTestingHelper.UserInfoBuilder(UserHandle.USER_SYSTEM)
+                .setFlags(UserInfo.FLAG_SYSTEM).build();
+        when(um.getUserInfo(UserHandle.USER_SYSTEM)).thenReturn(user);
     }
 
     /**
@@ -144,6 +171,40 @@
         when(binder.queryLocalInterface(anyString())).thenReturn(service);
     }
 
+    /**
+     * Mocks a call to {@link Context#getSystemService(Class)}.
+     */
+    public static <T> void mockContextGetService(@NonNull Context context,
+            @NonNull Class<T> serviceClass, @NonNull T service) {
+        when(context.getSystemService(serviceClass)).thenReturn(service);
+        if (serviceClass.equals(PackageManager.class)) {
+            when(context.getPackageManager()).thenReturn(PackageManager.class.cast(service));
+        }
+    }
+
+    /**
+     * Gets the result of a future, or throw a {@link IllegalStateException} if it times out after
+     * {@value #ASYNC_TIMEOUT_MS} ms.
+     */
+    @NonNull
+    public static <T> T getResult(@NonNull AndroidFuture<T> future)
+            throws InterruptedException, ExecutionException {
+        return getResult(future, ASYNC_TIMEOUT_MS);
+    }
+
+    /**
+     * Gets the result of a future, or throw a {@link IllegalStateException} if it times out.
+     */
+    @NonNull
+    public static <T> T getResult(@NonNull AndroidFuture<T> future, long timeoutMs)
+            throws InterruptedException, ExecutionException {
+        try {
+            return future.get(timeoutMs, TimeUnit.MILLISECONDS);
+        } catch (TimeoutException e) {
+            throw new IllegalStateException("not called in " + ASYNC_TIMEOUT_MS + "ms", e);
+        }
+    }
+
     private AndroidMockitoHelper() {
         throw new UnsupportedOperationException("contains only static methods");
     }
diff --git a/car-test-lib/src/android/car/test/mocks/JavaMockitoHelper.java b/car-test-lib/src/android/car/test/mocks/JavaMockitoHelper.java
index af50638..ab772a9 100644
--- a/car-test-lib/src/android/car/test/mocks/JavaMockitoHelper.java
+++ b/car-test-lib/src/android/car/test/mocks/JavaMockitoHelper.java
@@ -19,6 +19,7 @@
 import android.util.Log;
 
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -33,7 +34,7 @@
      *
      * @param timeoutMs how long to wait for
      *
-     * @throws IllegalStateException} if it times out.
+     * @throws {@link IllegalStateException} if it times out.
      */
     public static void await(@NonNull CountDownLatch latch, long timeoutMs)
             throws InterruptedException {
@@ -43,6 +44,20 @@
     }
 
     /**
+     * Waits for a semaphore.
+     *
+     * @param timeoutMs how long to wait for
+     *
+     * @throws {@link IllegalStateException} if it times out.
+     */
+    public static void await(@NonNull Semaphore semaphore, long timeoutMs)
+            throws InterruptedException {
+        if (!semaphore.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) {
+            throw new IllegalStateException(semaphore + " not released in " + timeoutMs + " ms");
+        }
+    }
+
+    /**
      * Silently waits for a latch to be counted down, without throwing any exception if it isn't.
      *
      * @param timeoutMs how long to wait for
diff --git a/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java b/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
index f50d80a..5ed572f 100644
--- a/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
+++ b/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
@@ -23,17 +23,30 @@
 import android.car.user.CarUserManager.UserLifecycleEvent;
 import android.car.user.CarUserManager.UserLifecycleEventType;
 import android.car.user.CarUserManager.UserLifecycleListener;
-import android.os.UserHandle;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
- * UserLifecycleListener that blocks until an event is received.
+ * {@link UserLifecycleListener} that blocks until the proper events are received.
+ *
+ * <p>It can be used in 2 "modes":
+ *
+ * <ul>
+ *   <li>{@link #forAnyEvent()}: it blocks (through the {@link #waitForAnyEvent()} call) until any
+ *   any event is received. It doesn't allow any customization (other than
+ *   {@link Builder#setTimeout(long)}).
+ *   <li>{@link #forSpecificEvents()}: it blocks (through the {@link #waitForEvents()} call) until
+ *   all events specified by the {@link Builder} are received.
+ * </ul>
  */
 public final class BlockingUserLifecycleListener implements UserLifecycleListener {
 
@@ -41,31 +54,55 @@
 
     private static final long DEFAULT_TIMEOUT_MS = 2_000;
 
+    private final Object mLock = new Object();
+
     private final CountDownLatch mLatch = new CountDownLatch(1);
+
+    @GuardedBy("mLock")
     private final List<UserLifecycleEvent> mAllReceivedEvents = new ArrayList<>();
+    @GuardedBy("mLock")
     private final List<UserLifecycleEvent> mExpectedEventsReceived = new ArrayList<>();
 
     @UserLifecycleEventType
     private final List<Integer> mExpectedEventTypes;
 
+    @UserLifecycleEventType
+    private final List<Integer> mExpectedEventTypesLeft;
+
     @UserIdInt
-    private final int mForUserId;
+    @Nullable
+    private final Integer mForUserId;
+
+    @UserIdInt
+    @Nullable
+    private final Integer mForPreviousUserId;
 
     private final long mTimeoutMs;
 
     private BlockingUserLifecycleListener(Builder builder) {
-        mExpectedEventTypes = builder.mExpectedEventTypes;
+        mExpectedEventTypes = Collections
+                .unmodifiableList(new ArrayList<>(builder.mExpectedEventTypes));
+        mExpectedEventTypesLeft = builder.mExpectedEventTypes;
         mTimeoutMs = builder.mTimeoutMs;
         mForUserId = builder.mForUserId;
+        mForPreviousUserId = builder.mForPreviousUserId;
+        Log.d(TAG, "constructor: " + this);
     }
 
     /**
-     * Builds a new instance with default timeout of {@value #DEFAULT_TIMEOUT_MS} and
-     * which waits for one - and any one - event.
+     * Creates a builder for tests that need to wait for an arbitrary event.
      */
     @NonNull
-    public static BlockingUserLifecycleListener newDefaultListener() {
-        return new Builder().build();
+    public static Builder forAnyEvent() {
+        return new Builder(/* forAnyEvent= */ true);
+    }
+
+    /**
+     * Creates a builder for tests that need to wait for specific events.
+     */
+    @NonNull
+    public static Builder forSpecificEvents() {
+        return new Builder(/* forAnyEvent= */ false);
     }
 
     /**
@@ -73,12 +110,22 @@
      */
     public static final class Builder {
         private long mTimeoutMs = DEFAULT_TIMEOUT_MS;
+        private final boolean mForAnyEvent;
+
+        private Builder(boolean forAnyEvent) {
+            mForAnyEvent = forAnyEvent;
+        }
 
         @UserLifecycleEventType
-        private List<Integer> mExpectedEventTypes = new ArrayList<>();
+        private final List<Integer> mExpectedEventTypes = new ArrayList<>();
 
         @UserIdInt
-        private int mForUserId = UserHandle.USER_NULL;
+        @Nullable
+        private Integer mForUserId;
+
+        @UserIdInt
+        @Nullable
+        private Integer mForPreviousUserId;
 
         /**
          * Sets the timeout.
@@ -90,8 +137,12 @@
 
         /**
          * Sets the expected type - once the given event is received, the listener will unblock.
+         *
+         * @throws IllegalStateException if builder is {@link #forAnyEvent}.
+         * @throws IllegalArgumentException if the expected type was already added.
          */
         public Builder addExpectedEvent(@UserLifecycleEventType int eventType) {
+            assertNotForAnyEvent();
             mExpectedEventTypes.add(eventType);
             return this;
         }
@@ -100,11 +151,21 @@
          * Filters received events just for the given user.
          */
         public Builder forUser(@UserIdInt int userId) {
+            assertNotForAnyEvent();
             mForUserId = userId;
             return this;
         }
 
         /**
+         * Filters received events just for the given previous user.
+         */
+        public Builder forPreviousUser(@UserIdInt int userId) {
+            assertNotForAnyEvent();
+            mForPreviousUserId = userId;
+            return this;
+        }
+
+        /**
          * Builds a new instance.
          */
         @NonNull
@@ -112,114 +173,144 @@
             return new BlockingUserLifecycleListener(Builder.this);
         }
 
+        private void assertNotForAnyEvent() {
+            Preconditions.checkState(!mForAnyEvent, "not allowed forAnyEvent()");
+        }
     }
 
     @Override
     public void onEvent(UserLifecycleEvent event) {
-        Log.d(TAG, "onEvent(): expecting=" + expectedEventsToString()
-                + ", received=" + event);
-        mAllReceivedEvents.add(event);
+        synchronized (mLock) {
+            Log.d(TAG, "onEvent(): expecting=" + mExpectedEventTypesLeft + ", received=" + event);
 
-        if (expectingSpecificUser() && event.getUserId() != mForUserId) {
-            Log.w(TAG, "ignoring event for different user");
-            return;
-        }
+            mAllReceivedEvents.add(event);
 
-        Integer actualType = event.getEventType();
-        boolean removed = mExpectedEventTypes.remove(actualType);
-        if (removed) {
-            Log.v(TAG, "event removed; still expecting for " + expectedEventsToString());
-            mExpectedEventsReceived.add(event);
-        } else {
-            Log.v(TAG, "event not removed");
-        }
+            if (expectingSpecificUser() && event.getUserId() != mForUserId) {
+                Log.w(TAG, "ignoring event for different user (expecting " + mForUserId + ")");
+                return;
+            }
 
-        if (mExpectedEventTypes.isEmpty()) {
-            Log.d(TAG, "all expected events received, counting down " + mLatch);
-            mLatch.countDown();
+            if (expectingSpecificPreviousUser()
+                    && event.getPreviousUserId() != mForPreviousUserId) {
+                Log.w(TAG, "ignoring event for different previous user (expecting "
+                        + mForPreviousUserId + ")");
+                return;
+            }
+
+            Integer actualType = event.getEventType();
+            boolean removed = mExpectedEventTypesLeft.remove(actualType);
+            if (removed) {
+                Log.v(TAG, "event removed; still expecting for "
+                        + toString(mExpectedEventTypesLeft));
+                mExpectedEventsReceived.add(event);
+            } else {
+                Log.v(TAG, "event not removed");
+            }
+
+            if (mExpectedEventTypesLeft.isEmpty() && mLatch.getCount() == 1) {
+                Log.d(TAG, "all expected events received, counting down " + mLatch);
+                mLatch.countDown();
+            }
         }
     }
 
     /**
-     * Helper method for {@link #waitForEvents()} when caller is expecting just one event.
+     * Blocks until any event is received, and returns it.
+     *
+     * @throws IllegalStateException if listener was built using {@link #forSpecificEvents()}.
+     * @throws IllegalStateException if it times out before any event is received.
+     * @throws InterruptedException if interrupted before any event is received.
      */
     @Nullable
-    public UserLifecycleEvent waitForEvent() throws InterruptedException {
-        List<UserLifecycleEvent> receivedEvents = waitForEvents();
-        UserLifecycleEvent event = receivedEvents.isEmpty() ? null : receivedEvents.get(0);
-        Log.v(TAG, "waitForEvent(): returning " + event);
+    public UserLifecycleEvent waitForAnyEvent() throws InterruptedException {
+        Preconditions.checkState(isForAnyEvent(),
+                "cannot call waitForEvent() when built with expected events");
+        waitForExpectedEvents();
+
+        UserLifecycleEvent event;
+        synchronized (mLock) {
+            event = mAllReceivedEvents.isEmpty() ? null : mAllReceivedEvents.get(0);
+            Log.v(TAG, "waitForAnyEvent(): returning " + event);
+        }
         return event;
     }
 
     /**
-     * Blocks until all expected {@link #onEvent(UserLifecycleEvent)} are received.
+     * Blocks until the events specified in the {@link Builder} are received, and returns them.
      *
-     * @throws IllegalStateException if it times out before all events are received.
-     * @throws InterruptedException if interrupted before all events are received.
+     * @throws IllegalStateException if listener was built without any call to
+     * {@link Builder#addExpectedEvent(int)} or using {@link #forAnyEvent().
+     * @throws IllegalStateException if it times out before all specified events are received.
+     * @throws InterruptedException if interrupted before all specified events are received.
      */
     @NonNull
     public List<UserLifecycleEvent> waitForEvents() throws InterruptedException {
-        if (!mLatch.await(mTimeoutMs, TimeUnit.MILLISECONDS)) {
-            String errorMessage = "did not receive all events in " + mTimeoutMs + " seconds; "
-                    + "received only " + allReceivedEventsToString() + ", still waiting for "
-                    + expectedEventsToString();
-            Log.e(TAG, errorMessage);
-            throw new IllegalStateException(errorMessage);
+        Preconditions.checkState(!isForAnyEvent(),
+                "cannot call waitForEvents() when built without specific expected events");
+        waitForExpectedEvents();
+        List<UserLifecycleEvent> events;
+        synchronized (mLock) {
+            events = mExpectedEventsReceived;
         }
-        Log.v(TAG, "waitForEvents(): returning " + mAllReceivedEvents);
-        return getAllReceivedEvents();
+        Log.v(TAG, "waitForEvents(): returning " + events);
+        return events;
     }
 
     /**
-     * Gets a list with all received events.
+     * Gets a list with all received events until now.
      */
     @NonNull
     public List<UserLifecycleEvent> getAllReceivedEvents() {
-        return mAllReceivedEvents;
-    }
-
-    /**
-     * Gets a list with just the received events set by the builder.
-     */
-    @NonNull
-    public List<UserLifecycleEvent> getExpectedEventsReceived() {
-        return mExpectedEventsReceived;
+        Preconditions.checkState(!isForAnyEvent(),
+                "cannot call getAllReceivedEvents() when built without specific expected events");
+        synchronized (mLock) {
+            return Collections.unmodifiableList(new ArrayList<>(mAllReceivedEvents));
+        }
     }
 
     @Override
     public String toString() {
-        return "[" + getClass().getSimpleName() + ": "
-                + "timeout=" + mTimeoutMs + "ms, "
-                + (expectingSpecificUser() ? "forUser=" + mForUserId : "")
-                + ",received=" + allReceivedEventsToString()
-                + ", waiting=" + expectedEventsToString()
-                + "]";
+        return "[" + getClass().getSimpleName() + ": " + stateToString() + "]";
     }
 
-    private String allReceivedEventsToString() {
-        String receivedEvents = mAllReceivedEvents
-                .stream()
-                .map((e) -> CarUserManager.lifecycleEventTypeToString(e.getEventType()))
-                .collect(Collectors.toList())
-                .toString();
-        return expectingSpecificUser()
-                ? "{user=" + mForUserId + ", events=" + receivedEvents + "}"
-                : receivedEvents;
+    @NonNull
+    private String stateToString() {
+        synchronized (mLock) {
+            return "timeout=" + mTimeoutMs + "ms"
+                    + ",expectedEventTypes=" + toString(mExpectedEventTypes)
+                    + ",expectedEventTypesLeft=" + toString(mExpectedEventTypesLeft)
+                    + (expectingSpecificUser() ? ",forUser=" + mForUserId : "")
+                    + (expectingSpecificPreviousUser() ? ",forPrevUser=" + mForPreviousUserId : "")
+                    + ",received=" + mAllReceivedEvents
+                    + ",waiting=" + mExpectedEventTypesLeft;
+        }
     }
 
-    private String expectedEventsToString() {
-        String expectedTypes = mExpectedEventTypes
-                .stream()
-                .map((type) -> CarUserManager.lifecycleEventTypeToString(type))
+    private void waitForExpectedEvents() throws InterruptedException {
+        if (!mLatch.await(mTimeoutMs, TimeUnit.MILLISECONDS)) {
+            String errorMessage = "did not receive all expected events (" + stateToString() + ")";
+            Log.e(TAG, errorMessage);
+            throw new IllegalStateException(errorMessage);
+        }
+    }
+
+    @NonNull
+    private static String toString(@NonNull List<Integer> eventTypes) {
+        return eventTypes.stream()
+                .map((i) -> CarUserManager.lifecycleEventTypeToString(i))
                 .collect(Collectors.toList())
                 .toString();
-        return expectingSpecificUser()
-                ? "{user=" + mForUserId + ", types=" + expectedTypes + "}"
-                : expectedTypes;
+    }
+
+    private boolean isForAnyEvent() {
+        return mExpectedEventTypes.isEmpty();
     }
 
     private boolean expectingSpecificUser() {
-        return mForUserId != UserHandle.USER_NULL;
+        return mForUserId != null;
     }
 
+    private boolean expectingSpecificPreviousUser() {
+        return mForPreviousUserId != null;
+    }
 }
diff --git a/car-test-lib/src/android/car/testapi/CarMockitoHelper.java b/car-test-lib/src/android/car/testapi/CarMockitoHelper.java
index d69a0f8..13c5361 100644
--- a/car-test-lib/src/android/car/testapi/CarMockitoHelper.java
+++ b/car-test-lib/src/android/car/testapi/CarMockitoHelper.java
@@ -22,12 +22,15 @@
 import android.annotation.NonNull;
 import android.car.Car;
 import android.os.RemoteException;
+import android.util.Log;
 
 /**
  * Provides common Mockito calls for Car-specific classes.
  */
 public final class CarMockitoHelper {
 
+    private static final String TAG = CarMockitoHelper.class.getSimpleName();
+
     /**
      * Mocks a call to {@link Car#handleRemoteExceptionFromCarService(RemoteException, Object)} so
      * it returns the passed as 2nd argument.
@@ -35,7 +38,9 @@
     public static void mockHandleRemoteExceptionFromCarServiceWithDefaultValue(
             @NonNull Car car) {
         doAnswer((invocation) -> {
-            return invocation.getArguments()[1];
+            Object returnValue = invocation.getArguments()[1];
+            Log.v(TAG, "mocking handleRemoteExceptionFromCarService(): " + returnValue);
+            return returnValue;
         }).when(car).handleRemoteExceptionFromCarService(isA(RemoteException.class), any());
     }
 
diff --git a/car_product/overlay/frameworks/base/core/res/res/drawable-night/item_background_material.xml b/car_product/overlay/frameworks/base/core/res/res/drawable-night/item_background_material.xml
new file mode 100644
index 0000000..cb36bb5
--- /dev/null
+++ b/car_product/overlay/frameworks/base/core/res/res/drawable-night/item_background_material.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="#2371cd">
+    <item android:id="@android:id/mask">
+        <color android:color="@android:color/white" />
+    </item>
+</ripple>
diff --git a/car_product/overlay/frameworks/base/core/res/res/drawable/item_background_material.xml b/car_product/overlay/frameworks/base/core/res/res/drawable/item_background_material.xml
new file mode 100644
index 0000000..9311499
--- /dev/null
+++ b/car_product/overlay/frameworks/base/core/res/res/drawable/item_background_material.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="#4b9eff">
+    <item android:id="@android:id/mask">
+        <color android:color="@android:color/white" />
+    </item>
+</ripple>
diff --git a/car_product/overlay/frameworks/base/core/res/res/layout/alert_dialog_button_bar_material.xml b/car_product/overlay/frameworks/base/core/res/res/layout/alert_dialog_button_bar_material.xml
index 0df7215..b227bec 100644
--- a/car_product/overlay/frameworks/base/core/res/res/layout/alert_dialog_button_bar_material.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/layout/alert_dialog_button_bar_material.xml
@@ -31,13 +31,13 @@
         android:paddingTop="@*android:dimen/button_bar_layout_top_padding"
         android:layoutDirection="locale"
         android:orientation="horizontal"
-        android:gravity="center_vertical">
+        android:gravity="left|center_vertical">
 
         <Button
             android:id="@*android:id/button3"
             style="@*android:style/CarAction1"
             android:background="@*android:drawable/car_dialog_button_background"
-            android:layout_marginEnd="@*android:dimen/button_end_margin"
+            android:layout_marginRight="@*android:dimen/button_end_margin"
             android:layout_width="wrap_content"
             android:layout_height="@*android:dimen/button_layout_height" />
 
@@ -45,7 +45,7 @@
             android:id="@*android:id/button2"
             style="@*android:style/CarAction1"
             android:background="@*android:drawable/car_dialog_button_background"
-            android:layout_marginEnd="@*android:dimen/button_end_margin"
+            android:layout_marginRight="@*android:dimen/button_end_margin"
             android:layout_width="wrap_content"
             android:layout_height="@*android:dimen/button_layout_height" />
 
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 4ba64f0..60e5e20 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
@@ -101,6 +101,11 @@
     <!-- Turn off Wallpaper service -->
     <bool name="config_enableWallpaperService">false</bool>
 
+    <!-- Flag specifying whether user-switch operations have custom UI. When false, user-switch
+         UI is handled by ActivityManagerService. On AAOS, this value should be true since the
+         UserSwitchUi is implemented by Car SystemUI.-->
+    <bool name="config_customUserSwitchUi">true</bool>
+
     <!-- Whether to only install system packages on a user if they're whitelisted for that user
          type. Override the default value in framework config file.
          0  - disable whitelist (install all system packages; no logging)
diff --git a/car_product/sepolicy/private/carservice_app.te b/car_product/sepolicy/private/carservice_app.te
index de8d874..d62cb27 100644
--- a/car_product/sepolicy/private/carservice_app.te
+++ b/car_product/sepolicy/private/carservice_app.te
@@ -69,6 +69,7 @@
 allow carservice_app sysfs_fs_lifetime_write:file { getattr open read };
 
 set_prop(carservice_app, ctl_start_prop)
+set_prop(carservice_app, ctl_stop_prop)
 unix_socket_connect(carservice_app, dumpstate, dumpstate)
 
 # Allow reading vehicle-specific configuration
diff --git a/evs/apps/default/Android.bp b/evs/apps/default/Android.bp
index 7fd60ef..df6fd0d 100644
--- a/evs/apps/default/Android.bp
+++ b/evs/apps/default/Android.bp
@@ -63,10 +63,6 @@
         "LabeledChecker.png",
     ],
 
-    strip: {
-        keep_symbols: true,
-    },
-
     init_rc: ["evs_app.rc"],
 
     cflags: ["-DLOG_TAG=\"EvsApp\""] + [
diff --git a/evs/apps/default/ConfigManager.h b/evs/apps/default/ConfigManager.h
index 6acd83d..9c6d1a2 100644
--- a/evs/apps/default/ConfigManager.h
+++ b/evs/apps/default/ConfigManager.h
@@ -19,6 +19,8 @@
 #include <vector>
 #include <string>
 
+#include <system/graphics-base.h>
+
 
 class ConfigManager {
 public:
@@ -91,6 +93,12 @@
     const DisplayInfo& getActiveDisplay() const { return mDisplays[mActiveDisplayId]; };
     void  useExternalMemory(bool flag) { mUseExternalMemory = flag; }
     bool  getUseExternalMemory() const { return mUseExternalMemory; }
+    void  setExternalMemoryFormat(android_pixel_format_t format) {
+        mExternalMemoryFormat = format;
+    }
+    android_pixel_format_t getExternalMemoryFormat() const {
+        return mExternalMemoryFormat;
+    }
 
 private:
     // Camera information
@@ -103,6 +111,9 @@
     // Memory management
     bool mUseExternalMemory;
 
+    // Format of external memory
+    android_pixel_format_t mExternalMemoryFormat;
+
     // Car body information (assumes front wheel steering and origin at center of rear axel)
     // Note that units aren't specified and don't matter as long as all length units are consistent
     // within the JSON file from which we parse.  That is, if everything is in meters, that's fine.
diff --git a/evs/apps/default/RenderDirectView.cpp b/evs/apps/default/RenderDirectView.cpp
index 2938521..68b731e 100644
--- a/evs/apps/default/RenderDirectView.cpp
+++ b/evs/apps/default/RenderDirectView.cpp
@@ -116,7 +116,8 @@
                                       mCameraDesc.v1.cameraId.c_str(),
                                       foundCfg ? std::move(targetCfg) : nullptr,
                                       sDisplay,
-                                      mConfig.getUseExternalMemory()));
+                                      mConfig.getUseExternalMemory(),
+                                      mConfig.getExternalMemoryFormat()));
     if (!mTexture) {
         LOG(ERROR) << "Failed to set up video texture for " << mCameraDesc.v1.cameraId;
 // TODO:  For production use, we may actually want to fail in this case, but not yet...
diff --git a/evs/apps/default/StreamHandler.cpp b/evs/apps/default/StreamHandler.cpp
index b1cfd1f..d350af1 100644
--- a/evs/apps/default/StreamHandler.cpp
+++ b/evs/apps/default/StreamHandler.cpp
@@ -30,6 +30,7 @@
 StreamHandler::StreamHandler(android::sp <IEvsCamera> pCamera,
                              uint32_t numBuffers,
                              bool useOwnBuffers,
+                             android_pixel_format_t format,
                              int32_t width,
                              int32_t height)
     : mCamera(pCamera),
@@ -46,7 +47,6 @@
         const auto usage = GRALLOC_USAGE_HW_TEXTURE |
                            GRALLOC_USAGE_SW_READ_RARELY |
                            GRALLOC_USAGE_SW_WRITE_OFTEN;
-        const auto format = HAL_PIXEL_FORMAT_RGBA_8888;
         for (auto i = 0; i < numBuffers; ++i) {
             unsigned pixelsPerLine;
             android::status_t result = alloc.allocate(width,
@@ -64,10 +64,10 @@
                 BufferDesc_1_1 buf;
                 AHardwareBuffer_Desc* pDesc =
                     reinterpret_cast<AHardwareBuffer_Desc *>(&buf.buffer.description);
-                pDesc->width = 640;
-                pDesc->height = 360;
+                pDesc->width = width;
+                pDesc->height = height;
                 pDesc->layers = 1;
-                pDesc->format = HAL_PIXEL_FORMAT_RGBA_8888;
+                pDesc->format = format;
                 pDesc->usage = GRALLOC_USAGE_HW_TEXTURE |
                                GRALLOC_USAGE_SW_READ_RARELY |
                                GRALLOC_USAGE_SW_WRITE_OFTEN;
diff --git a/evs/apps/default/StreamHandler.h b/evs/apps/default/StreamHandler.h
index f877c78..cb22b36 100644
--- a/evs/apps/default/StreamHandler.h
+++ b/evs/apps/default/StreamHandler.h
@@ -51,6 +51,7 @@
     StreamHandler(android::sp <IEvsCamera> pCamera,
                   uint32_t numBuffers = 2,
                   bool useOwnBuffers = false,
+                  android_pixel_format_t format = HAL_PIXEL_FORMAT_RGBA_8888,
                   int32_t width = 640,
                   int32_t height = 360);
     void shutdown();
diff --git a/evs/apps/default/VideoTex.cpp b/evs/apps/default/VideoTex.cpp
index 94e734a..7491dfe 100644
--- a/evs/apps/default/VideoTex.cpp
+++ b/evs/apps/default/VideoTex.cpp
@@ -137,29 +137,39 @@
                              const char* evsCameraId,
                              std::unique_ptr<Stream> streamCfg,
                              EGLDisplay glDisplay,
-                             bool useExternalMemory) {
+                             bool useExternalMemory,
+                             android_pixel_format_t format) {
     // Set up the camera to feed this texture
     sp<IEvsCamera> pCamera = nullptr;
+    sp<StreamHandler> pStreamHandler = nullptr;
     if (streamCfg != nullptr) {
         pCamera = pEnum->openCamera_1_1(evsCameraId, *streamCfg);
+
+        // Initialize the stream that will help us update this texture's contents
+        pStreamHandler = new StreamHandler(pCamera,
+                                           2,     // number of buffers
+                                           useExternalMemory,
+                                           format,
+                                           streamCfg->width,
+                                           streamCfg->height);
     } else {
         pCamera =
             IEvsCamera::castFrom(pEnum->openCamera(evsCameraId))
             .withDefault(nullptr);
+
+        // Initialize the stream with the default resolution
+        pStreamHandler = new StreamHandler(pCamera,
+                                           2,     // number of buffers
+                                           useExternalMemory,
+                                           format);
     }
 
-    if (pCamera.get() == nullptr) {
+    if (pCamera == nullptr) {
         LOG(ERROR) << "Failed to allocate new EVS Camera interface for " << evsCameraId;
         return nullptr;
     }
 
-    // Initialize the stream that will help us update this texture's contents
-    sp<StreamHandler> pStreamHandler = new StreamHandler(pCamera,
-                                                         2,     // number of buffers
-                                                         useExternalMemory,
-                                                         streamCfg->width,
-                                                         streamCfg->height);
-    if (pStreamHandler.get() == nullptr) {
+    if (pStreamHandler == nullptr) {
         LOG(ERROR) << "Failed to allocate FrameHandler";
         return nullptr;
     }
diff --git a/evs/apps/default/VideoTex.h b/evs/apps/default/VideoTex.h
index d884faa..097d086 100644
--- a/evs/apps/default/VideoTex.h
+++ b/evs/apps/default/VideoTex.h
@@ -16,6 +16,9 @@
 #ifndef VIDEOTEX_H
 #define VIDEOTEX_H
 
+#include "StreamHandler.h"
+#include "TexWrapper.h"
+
 #include <EGL/egl.h>
 #include <EGL/eglext.h>
 #include <GLES2/gl2.h>
@@ -25,9 +28,7 @@
 
 #include <android/hardware/automotive/evs/1.1/IEvsEnumerator.h>
 #include <android/hardware/camera/device/3.2/ICameraDevice.h>
-
-#include "TexWrapper.h"
-#include "StreamHandler.h"
+#include <system/graphics-base.h>
 
 using ::android::hardware::camera::device::V3_2::Stream;
 using namespace ::android::hardware::automotive::evs::V1_1;
@@ -38,7 +39,8 @@
                                         const char *evsCameraId,
                                         std::unique_ptr<Stream> streamCfg,
                                         EGLDisplay glDisplay,
-                                        bool useExternalMemory);
+                                        bool useExternalMemory,
+                                        android_pixel_format_t format);
 
 public:
     VideoTex() = delete;
@@ -62,10 +64,13 @@
 };
 
 
+// Creates a video texture to draw the camera preview.  format is effective only
+// when useExternalMemory is true.
 VideoTex* createVideoTexture(sp<IEvsEnumerator> pEnum,
                              const char * deviceName,
                              std::unique_ptr<Stream> streamCfg,
                              EGLDisplay glDisplay,
-                             bool useExternalMemory = false);
+                             bool useExternalMemory = false,
+                             android_pixel_format_t format = HAL_PIXEL_FORMAT_RGBA_8888);
 
 #endif // VIDEOTEX_H
diff --git a/evs/apps/default/evs_app.cpp b/evs/apps/default/evs_app.cpp
index a968990..9f2f2c8 100644
--- a/evs/apps/default/evs_app.cpp
+++ b/evs/apps/default/evs_app.cpp
@@ -14,24 +14,25 @@
  * limitations under the License.
  */
 
+#include "ConfigManager.h"
+#include "EvsStateControl.h"
+#include "EvsVehicleListener.h"
+
 #include <stdio.h>
 
+#include <android/hardware/automotive/evs/1.1/IEvsDisplay.h>
+#include <android/hardware/automotive/evs/1.1/IEvsEnumerator.h>
+#include <android-base/logging.h>
+#include <android-base/macros.h>    // arraysize
+#include <android-base/strings.h>
 #include <hidl/HidlTransportSupport.h>
+#include <hwbinder/ProcessState.h>
 #include <utils/Errors.h>
 #include <utils/StrongPointer.h>
 #include <utils/Log.h>
 
-#include "android-base/macros.h"    // arraysize
-#include "android-base/logging.h"
 
-#include <android/hardware/automotive/evs/1.1/IEvsEnumerator.h>
-#include <android/hardware/automotive/evs/1.1/IEvsDisplay.h>
-
-#include <hwbinder/ProcessState.h>
-
-#include "EvsStateControl.h"
-#include "EvsVehicleListener.h"
-#include "ConfigManager.h"
+using android::base::EqualsIgnoreCase;
 
 // libhidl:
 using android::hardware::configureRpcThreadpool;
@@ -66,6 +67,24 @@
 }
 
 
+static bool convertStringToFormat(const char* str, android_pixel_format_t* output) {
+    bool result = true;
+    if (EqualsIgnoreCase(str, "RGBA8888")) {
+        *output = HAL_PIXEL_FORMAT_RGBA_8888;
+    } else if (EqualsIgnoreCase(str, "YV12")) {
+        *output = HAL_PIXEL_FORMAT_YV12;
+    } else if (EqualsIgnoreCase(str, "NV21")) {
+        *output = HAL_PIXEL_FORMAT_YCrCb_420_SP;
+    } else if (EqualsIgnoreCase(str, "YUYV")) {
+        *output = HAL_PIXEL_FORMAT_YCBCR_422_I;
+    } else {
+        result = false;
+    }
+
+    return result;
+}
+
+
 // Main entry point
 int main(int argc, char** argv)
 {
@@ -77,6 +96,7 @@
     const char* evsServiceName = "default";
     int displayId = 1;
     bool useExternalMemory = false;
+    android_pixel_format_t extMemoryFormat = HAL_PIXEL_FORMAT_RGBA_8888;
     for (int i=1; i< argc; i++) {
         if (strcmp(argv[i], "--test") == 0) {
             useVehicleHal = false;
@@ -90,6 +110,19 @@
             displayId = std::stoi(argv[++i]);
         } else if (strcmp(argv[i], "--extmem") == 0) {
             useExternalMemory = true;
+            if (i + 1 >= argc) {
+                // use RGBA8888 by default
+                LOG(INFO) << "External buffer format is not set.  "
+                          << "RGBA8888 will be used.";
+            } else {
+                if (!convertStringToFormat(argv[i + 1], &extMemoryFormat)) {
+                    LOG(WARNING) << "Color format string " << argv[i + 1]
+                                 << " is unknown or not supported.  RGBA8888 will be used.";
+                } else {
+                    // move the index
+                    ++i;
+                }
+            }
         } else {
             printf("Ignoring unrecognized command line arg '%s'\n", argv[i]);
             printHelp = true;
@@ -97,11 +130,23 @@
     }
     if (printHelp) {
         printf("Options include:\n");
-        printf("  --test    Do not talk to Vehicle Hal, but simulate 'reverse' instead\n");
-        printf("  --hw      Bypass EvsManager by connecting directly to EvsEnumeratorHw\n");
-        printf("  --mock    Connect directly to EvsEnumeratorHw-Mock\n");
-        printf("  --display Specify the display to use\n");
-        printf("  --extmem  Application allocates buffers to capture camera frames\n");
+        printf("  --test\n\tDo not talk to Vehicle Hal, but simulate 'reverse' instead\n");
+        printf("  --hw\n\tBypass EvsManager by connecting directly to EvsEnumeratorHw\n");
+        printf("  --mock\n\tConnect directly to EvsEnumeratorHw-Mock\n");
+        printf("  --display\n\tSpecify the display to use\n");
+        printf("  --extmem  <format>\n\t"
+               "Application allocates buffers to capture camera frames.  "
+               "Available format strings are (case insensitive):\n");
+        printf("\t\tRGBA8888: 4x8-bit RGBA format.  This is the default format to be used "
+               "when no format is specified.\n");
+        printf("\t\tYV12: YUV420 planar format with a full resolution Y plane "
+               "followed by a V values, with U values last.\n");
+        printf("\t\tNV21: A biplanar format with a full resolution Y plane "
+               "followed by a single chrome plane with weaved V and U values.\n");
+        printf("\t\tYUYV: Packed format with a half horizontal chrome resolution.  "
+               "Known as YUV4:2:2.\n");
+
+        return EXIT_FAILURE;
     }
 
     // Load our configuration information
@@ -140,6 +185,7 @@
     }
     config.setActiveDisplayId(displayId);
     config.useExternalMemory(useExternalMemory);
+    config.setExternalMemoryFormat(extMemoryFormat);
 
     // Connect to the Vehicle HAL so we can monitor state
     sp<IVehicle> pVnet;
diff --git a/evs/apps/demo_app_evs_support_lib/Android.bp b/evs/apps/demo_app_evs_support_lib/Android.bp
index 5380a4a..ae0073a 100644
--- a/evs/apps/demo_app_evs_support_lib/Android.bp
+++ b/evs/apps/demo_app_evs_support_lib/Android.bp
@@ -30,10 +30,6 @@
 
     include_dirs: ["packages/services/Car/evs/support_library"],
 
-    strip: {
-        keep_symbols: true,
-    },
-
     init_rc: ["evs_app_support_lib.rc"],
 
     cflags: ["-DLOG_TAG=\"EvsAppSupportLib\""] + [
diff --git a/evs/manager/1.0/Android.bp b/evs/manager/1.0/Android.bp
index 260e0b7..82bcdd8 100644
--- a/evs/manager/1.0/Android.bp
+++ b/evs/manager/1.0/Android.bp
@@ -39,10 +39,6 @@
 
     init_rc: ["android.automotive.evs.manager@1.0.rc"],
 
-    strip: {
-        keep_symbols: true,
-    },
-
     cflags: ["-DLOG_TAG=\"EvsManagerV1_0\""] + [
         "-DGL_GLEXT_PROTOTYPES",
         "-DEGL_EGLEXT_PROTOTYPES",
diff --git a/evs/manager/1.1/Android.bp b/evs/manager/1.1/Android.bp
index 694aa06..891d615 100644
--- a/evs/manager/1.1/Android.bp
+++ b/evs/manager/1.1/Android.bp
@@ -20,35 +20,36 @@
     name: "android.automotive.evs.manager@1.1",
 
     srcs: [
-        "service.cpp",
         "Enumerator.cpp",
         "HalCamera.cpp",
-        "VirtualCamera.cpp",
         "HalDisplay.cpp",
+        "VirtualCamera.cpp",
+        "service.cpp",
+        "stats/CameraUsageStats.cpp",
+        "stats/LooperWrapper.cpp",
+        "stats/StatsCollector.cpp",
         "sync/unique_fd.cpp",
         "sync/unique_fence.cpp",
         "sync/unique_timeline.cpp",
     ],
 
     shared_libs: [
-        "libbase",
-        "libcutils",
-        "libutils",
-        "libui",
-        "libsync",
-        "libhidlbase",
-        "libhardware",
-        "libcamera_metadata",
         "android.hardware.automotive.evs@1.0",
         "android.hardware.automotive.evs@1.1",
+        "libbase",
+        "libcamera_metadata",
+        "libcutils",
+        "libhardware",
+        "libhidlbase",
+        "libprocessgroup",
+        "libstatslog",
+        "libsync",
+        "libui",
+        "libutils",
     ],
 
     init_rc: ["android.automotive.evs.manager@1.1.rc"],
 
-    strip: {
-        keep_symbols: true,
-    },
-
     cflags: ["-DLOG_TAG=\"EvsManagerV1_1\""] + [
         "-DGL_GLEXT_PROTOTYPES",
         "-DEGL_EGLEXT_PROTOTYPES",
diff --git a/evs/manager/1.1/Enumerator.cpp b/evs/manager/1.1/Enumerator.cpp
index 8417828..fdf3500 100644
--- a/evs/manager/1.1/Enumerator.cpp
+++ b/evs/manager/1.1/Enumerator.cpp
@@ -14,22 +14,50 @@
  * limitations under the License.
  */
 
-#include <android-base/parseint.h>
-#include <android-base/strings.h>
-#include <android-base/logging.h>
-#include <hwbinder/IPCThreadState.h>
-#include <cutils/android_filesystem_config.h>
-
 #include "Enumerator.h"
 #include "HalDisplay.h"
 
+#include <android-base/chrono_utils.h>
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/strings.h>
+#include <android-base/stringprintf.h>
+#include <cutils/android_filesystem_config.h>
+#include <hwbinder/IPCThreadState.h>
+
+namespace {
+
+    const char* kSingleIndent = "\t";
+    const char* kDumpOptionAll = "all";
+    const char* kDumpDeviceCamera = "camera";
+    const char* kDumpDeviceDisplay = "display";
+
+    const char* kDumpCameraCommandCurrent = "--current";
+    const char* kDumpCameraCommandCollected = "--collected";
+    const char* kDumpCameraCommandCustom = "--custom";
+    const char* kDumpCameraCommandCustomStart = "start";
+    const char* kDumpCameraCommandCustomStop = "stop";
+
+    const int kDumpCameraMinNumArgs = 4;
+    const int kOptionDumpDeviceTypeIndex = 1;
+    const int kOptionDumpCameraTypeIndex = 2;
+    const int kOptionDumpCameraCommandIndex = 3;
+    const int kOptionDumpCameraArgsStartIndex = 4;
+
+}
+
 namespace android {
 namespace automotive {
 namespace evs {
 namespace V1_1 {
 namespace implementation {
 
+using ::android::base::Error;
 using ::android::base::EqualsIgnoreCase;
+using ::android::base::StringAppendF;
+using ::android::base::StringPrintf;
+using ::android::base::WriteStringToFd;
 using CameraDesc_1_0 = ::android::hardware::automotive::evs::V1_0::CameraDesc;
 using CameraDesc_1_1 = ::android::hardware::automotive::evs::V1_1::CameraDesc;
 
@@ -43,15 +71,32 @@
         // Get an internal display identifier.
         mHwEnumerator->getDisplayIdList(
             [this](const auto& displayPorts) {
-                if (displayPorts.size() > 0) {
-                    mInternalDisplayPort = displayPorts[0];
-                } else {
+                for (auto& port : displayPorts) {
+                    mDisplayPorts.push_back(port);
+                }
+
+                // The first element is the internal display
+                mInternalDisplayPort = mDisplayPorts.front();
+                if (mDisplayPorts.size() < 1) {
                     LOG(WARNING) << "No display is available to EVS service.";
                 }
             }
         );
     }
 
+    // Starts the statistics collection
+    mMonitorEnabled = false;
+    mClientsMonitor = new StatsCollector();
+    if (mClientsMonitor != nullptr) {
+        auto result = mClientsMonitor->startCollection();
+        if (!result.ok()) {
+            LOG(ERROR) << "Failed to start the usage monitor: "
+                       << result.error();
+        } else {
+            mMonitorEnabled = true;
+        }
+    }
+
     return result;
 }
 
@@ -188,7 +233,10 @@
         if (device == nullptr) {
             LOG(ERROR) << "Failed to open hardware camera " << cameraId;
         } else {
-            hwCamera = new HalCamera(device, cameraId);
+            // Calculates the usage statistics record identifier
+            auto fn = mCameraDevices.hash_function();
+            auto recordId = fn(cameraId) & 0xFF;
+            hwCamera = new HalCamera(device, cameraId, recordId);
             if (hwCamera == nullptr) {
                 LOG(ERROR) << "Failed to allocate camera wrapper object";
                 mHwEnumerator->closeCamera(device);
@@ -239,6 +287,9 @@
             // NOTE:  This should drop our last reference to the camera, resulting in its
             //        destruction.
             mActiveCameras.erase(halCamera->getId());
+            if (mMonitorEnabled) {
+                mClientsMonitor->unregisterClientToMonitor(halCamera->getId());
+            }
         }
     }
 
@@ -277,7 +328,10 @@
                 success = false;
                 break;
             } else {
-                hwCamera = new HalCamera(device, id, streamCfg);
+                // Calculates the usage statistics record identifier
+                auto fn = mCameraDevices.hash_function();
+                auto recordId = fn(id) & 0xFF;
+                hwCamera = new HalCamera(device, id, recordId, streamCfg);
                 if (hwCamera == nullptr) {
                     LOG(ERROR) << "Failed to allocate camera wrapper object";
                     mHwEnumerator->closeCamera(device);
@@ -295,6 +349,10 @@
 
             // Add the hardware camera to our list, which will keep it alive via ref count
             mActiveCameras.try_emplace(id, hwCamera);
+            if (mMonitorEnabled) {
+                mClientsMonitor->registerClientToMonitor(hwCamera);
+            }
+
             sourceCameras.push_back(hwCamera);
         } else {
             if (it->second->getStreamConfig().id != streamCfg.id) {
@@ -392,7 +450,7 @@
     // TODO: Because of b/129284474, an additional class, HalDisplay, has been defined and
     // wraps the IEvsDisplay object the driver returns.  We may want to remove this
     // additional class when it is fixed properly.
-    sp<IEvsDisplay_1_0> pHalDisplay = new HalDisplay(pActiveDisplay);
+    sp<IEvsDisplay_1_0> pHalDisplay = new HalDisplay(pActiveDisplay, mInternalDisplayPort);
     mActiveDisplay = pHalDisplay;
 
     return pHalDisplay;
@@ -444,6 +502,11 @@
         return nullptr;
     }
 
+    if (std::find(mDisplayPorts.begin(), mDisplayPorts.end(), id) == mDisplayPorts.end()) {
+        LOG(ERROR) << "No display is available on the port " << static_cast<int32_t>(id);
+        return nullptr;
+    }
+
     // We simply keep track of the most recently opened display instance.
     // In the underlying layers we expect that a new open will cause the previous
     // object to be destroyed.  This avoids any race conditions associated with
@@ -462,7 +525,7 @@
     // TODO: Because of b/129284474, an additional class, HalDisplay, has been defined and
     // wraps the IEvsDisplay object the driver returns.  We may want to remove this
     // additional class when it is fixed properly.
-    sp<IEvsDisplay_1_1> pHalDisplay = new HalDisplay(pActiveDisplay);
+    sp<IEvsDisplay_1_1> pHalDisplay = new HalDisplay(pActiveDisplay, id);
     mActiveDisplay = pHalDisplay;
 
     return pHalDisplay;
@@ -504,7 +567,7 @@
     if (fd.getNativeHandle() != nullptr && fd->numFds > 0) {
         cmdDump(fd->data[0], options);
     } else {
-        LOG(ERROR) << "Invalid parameters";
+        LOG(ERROR) << "Given file descriptor is not valid.";
     }
 
     return {};
@@ -513,7 +576,8 @@
 
 void Enumerator::cmdDump(int fd, const hidl_vec<hidl_string>& options) {
     if (options.size() == 0) {
-        dprintf(fd, "No option is given");
+        WriteStringToFd("No option is given.\n", fd);
+        cmdHelp(fd);
         return;
     }
 
@@ -525,19 +589,26 @@
     } else if (EqualsIgnoreCase(option, "--dump")) {
         cmdDumpDevice(fd, options);
     } else {
-        dprintf(fd, "Invalid option: %s\n", option.c_str());
+        WriteStringToFd(StringPrintf("Invalid option: %s\n", option.c_str()),
+                        fd);
     }
 }
 
 
 void Enumerator::cmdHelp(int fd) {
-    dprintf(fd, "Usage: \n\n");
-    dprintf(fd, "--help: shows this help.\n");
-    dprintf(fd, "--list [all|camera|display]: list camera or display devices or both "
-                "available to EVS manager.\n");
-    dprintf(fd, "--dump [all|camera|display] <device id>: "
-                "show current status of the target device or all devices "
-                "when no device is given.\n");
+    WriteStringToFd("--help: shows this help.\n"
+                    "--list [all|camera|display]: lists camera or display devices or both "
+                    "available to EVS manager.\n"
+                    "--dump camera [all|device_id] --[current|collected|custom] [args]\n"
+                    "\tcurrent: shows the current status\n"
+                    "\tcollected: shows 10 most recent periodically collected camera usage "
+                    "statistics\n"
+                    "\tcustom: starts/stops collecting the camera usage statistics\n"
+                    "\t\tstart [interval] [duration]: starts collecting usage statistics "
+                    "at every [interval] during [duration].  Interval and duration are in "
+                    "milliseconds.\n"
+                    "\t\tstop: stops collecting usage statistics and shows collected records.\n"
+                    "--dump display: shows current status of the display\n", fd);
 }
 
 
@@ -546,18 +617,25 @@
     bool listDisplays = true;
     if (options.size() > 1) {
         const std::string option = options[1];
-        const bool listAll = EqualsIgnoreCase(option, "all");
-        listCameras = listAll || EqualsIgnoreCase(option, "camera");
-        listDisplays = listAll || EqualsIgnoreCase(option, "display");
+        const bool listAll = EqualsIgnoreCase(option, kDumpOptionAll);
+        listCameras = listAll || EqualsIgnoreCase(option, kDumpDeviceCamera);
+        listDisplays = listAll || EqualsIgnoreCase(option, kDumpDeviceDisplay);
         if (!listCameras && !listDisplays) {
-            dprintf(fd, "Unrecognized option, %s, is ignored.\n", option.c_str());
+            WriteStringToFd(StringPrintf("Unrecognized option, %s, is ignored.\n",
+                                         option.c_str()),
+                            fd);
+
+            // Nothing to show, return
+            return;
         }
     }
 
+    std::string buffer;
     if (listCameras) {
-        dprintf(fd, "Camera devices available to EVS service:\n");
+        StringAppendF(&buffer,"Camera devices available to EVS service:\n");
         if (mCameraDevices.size() < 1) {
-            // Camera devices may not be enumerated yet.
+            // Camera devices may not be enumerated yet.  This may fail if the
+            // user is not permitted to use EVS service.
             getCameraList_1_1(
                 [](const auto cameras) {
                     if (cameras.size() < 1) {
@@ -567,62 +645,209 @@
         }
 
         for (auto& [id, desc] : mCameraDevices) {
-            dprintf(fd, "\t%s\n", id.c_str());
+            StringAppendF(&buffer, "%s%s\n", kSingleIndent, id.c_str());
         }
 
-        dprintf(fd, "\nCamera devices currently in use:\n");
+        StringAppendF(&buffer, "%sCamera devices currently in use:\n", kSingleIndent);
         for (auto& [id, ptr] : mActiveCameras) {
-            dprintf(fd, "\t%s\n", id.c_str());
+            StringAppendF(&buffer, "%s%s\n", kSingleIndent, id.c_str());
         }
-        dprintf(fd, "\n");
+        StringAppendF(&buffer, "\n");
     }
 
     if (listDisplays) {
         if (mHwEnumerator != nullptr) {
-            dprintf(fd, "Display devices available to EVS service:\n");
+            StringAppendF(&buffer, "Display devices available to EVS service:\n");
             // Get an internal display identifier.
             mHwEnumerator->getDisplayIdList(
                 [&](const auto& displayPorts) {
-                    for (auto& port : displayPorts) {
-                        dprintf(fd, "\tdisplay port %u\n", (unsigned)port);
+                    for (auto&& port : displayPorts) {
+                        StringAppendF(&buffer, "%sdisplay port %u\n",
+                                               kSingleIndent,
+                                               static_cast<unsigned>(port));
                     }
                 }
             );
+        } else {
+            LOG(WARNING) << "EVS HAL implementation is not available.";
         }
     }
+
+    WriteStringToFd(buffer, fd);
 }
 
 
 void Enumerator::cmdDumpDevice(int fd, const hidl_vec<hidl_string>& options) {
-    bool dumpCameras = true;
-    bool dumpDisplays = true;
-    if (options.size() > 1) {
-        const std::string option = options[1];
-        const bool dumpAll = EqualsIgnoreCase(option, "all");
-        dumpCameras = dumpAll || EqualsIgnoreCase(option, "camera");
-        dumpDisplays = dumpAll || EqualsIgnoreCase(option, "display");
+    // Dumps both cameras and displays if the target device type is not given
+    bool dumpCameras = false;
+    bool dumpDisplays = false;
+    const auto numOptions = options.size();
+    if (numOptions > kOptionDumpDeviceTypeIndex) {
+        const std::string target = options[kOptionDumpDeviceTypeIndex];
+        dumpCameras = EqualsIgnoreCase(target, kDumpDeviceCamera);
+        dumpDisplays = EqualsIgnoreCase(target, kDumpDeviceDisplay);
         if (!dumpCameras && !dumpDisplays) {
-            dprintf(fd, "Unrecognized option, %s, is ignored.\n", option.c_str());
+            WriteStringToFd(StringPrintf("Unrecognized option, %s, is ignored.\n",
+                                         target.c_str()),
+                            fd);
+            cmdHelp(fd);
+            return;
         }
+    } else {
+        WriteStringToFd(StringPrintf("Necessary arguments are missing.  "
+                                     "Please check the usages:\n"),
+                        fd);
+        cmdHelp(fd);
+        return;
     }
 
     if (dumpCameras) {
-        const bool dumpAllCameras = options.size() < 3;
-        std::string deviceId = "";
-        if (!dumpAllCameras) {
-            deviceId = options[2];
+        // --dump camera [all|device_id] --[current|collected|custom] [args]
+        if (numOptions < kDumpCameraMinNumArgs) {
+            WriteStringToFd(StringPrintf("Necessary arguments are missing.  "
+                                         "Please check the usages:\n"),
+                            fd);
+            cmdHelp(fd);
+            return;
         }
 
-        for (auto& [id, ptr] : mActiveCameras) {
-            if (!dumpAllCameras && !EqualsIgnoreCase(id, deviceId)) {
-                continue;
-            }
-            ptr->dump(fd);
+        const std::string deviceId = options[kOptionDumpCameraTypeIndex];
+        auto target = mActiveCameras.find(deviceId);
+        const bool dumpAllCameras = EqualsIgnoreCase(deviceId,
+                                                     kDumpOptionAll);
+        if (!dumpAllCameras && target == mActiveCameras.end()) {
+            // Unknown camera identifier
+            WriteStringToFd(StringPrintf("Given camera ID %s is unknown or not active.\n",
+                                         deviceId.c_str()),
+                            fd);
+            return;
         }
+
+        const std::string command = options[kOptionDumpCameraCommandIndex];
+        std::string cameraInfo;
+        if (EqualsIgnoreCase(command, kDumpCameraCommandCurrent)) {
+            // Active stream configuration from each active HalCamera objects
+            if (!dumpAllCameras) {
+                StringAppendF(&cameraInfo, "HalCamera: %s\n%s",
+                                           deviceId.c_str(),
+                                           target->second->toString(kSingleIndent).c_str());
+            } else {
+                for (auto&& [id, handle] : mActiveCameras) {
+                    // Appends the current status
+                    cameraInfo += handle->toString(kSingleIndent);
+                }
+            }
+        } else if (EqualsIgnoreCase(command, kDumpCameraCommandCollected)) {
+            // Reads the usage statistics from active HalCamera objects
+            std::unordered_map<std::string, std::string> usageStrings;
+            if (mMonitorEnabled) {
+                auto result = mClientsMonitor->toString(&usageStrings, kSingleIndent);
+                if (!result.ok()) {
+                    LOG(ERROR) << "Failed to get the monitoring result";
+                    return;
+                }
+
+                if (!dumpAllCameras) {
+                    cameraInfo += usageStrings[deviceId];
+                } else {
+                    for (auto&& [id, stats] : usageStrings) {
+                        cameraInfo += stats;
+                    }
+                }
+            } else {
+                WriteStringToFd(StringPrintf("Client monitor is not available.\n"),
+                                fd);
+                return;
+            }
+        } else if (EqualsIgnoreCase(command, kDumpCameraCommandCustom)) {
+            // Additional arguments are expected for this command:
+            // --dump camera device_id --custom start [interval] [duration]
+            // or, --dump camera device_id --custom stop
+            if (numOptions < kDumpCameraMinNumArgs + 1) {
+                WriteStringToFd(StringPrintf("Necessary arguments are missing. "
+                                             "Please check the usages:\n"),
+                                fd);
+                cmdHelp(fd);
+                return;
+            }
+
+            if (!mMonitorEnabled) {
+                WriteStringToFd(StringPrintf("Client monitor is not available."), fd);
+                return;
+            }
+
+            const std::string subcommand = options[kOptionDumpCameraArgsStartIndex];
+            if (EqualsIgnoreCase(subcommand, kDumpCameraCommandCustomStart)) {
+                using std::chrono::nanoseconds;
+                using std::chrono::milliseconds;
+                using std::chrono::duration_cast;
+                nanoseconds interval = 0ns;
+                nanoseconds duration = 0ns;
+                if (numOptions > kOptionDumpCameraArgsStartIndex + 2) {
+                    duration = duration_cast<nanoseconds>(
+                            milliseconds(
+                                    std::stoi(options[kOptionDumpCameraArgsStartIndex + 2])
+                            ));
+                }
+
+                if (numOptions > kOptionDumpCameraArgsStartIndex + 1) {
+                    interval = duration_cast<nanoseconds>(
+                            milliseconds(
+                                    std::stoi(options[kOptionDumpCameraArgsStartIndex + 1])
+                            ));
+                }
+
+                // Starts a custom collection
+                auto result = mClientsMonitor->startCustomCollection(interval, duration);
+                if (!result) {
+                    LOG(ERROR) << "Failed to start a custom collection.  "
+                               << result.error();
+                    StringAppendF(&cameraInfo, "Failed to start a custom collection. %s\n",
+                                               result.error().message().c_str());
+                }
+            } else if (EqualsIgnoreCase(subcommand, kDumpCameraCommandCustomStop)) {
+                if (!mMonitorEnabled) {
+                    WriteStringToFd(StringPrintf("Client monitor is not available."), fd);
+                    return;
+                }
+
+                auto result = mClientsMonitor->stopCustomCollection(deviceId);
+                if (!result) {
+                    LOG(ERROR) << "Failed to stop a custom collection.  "
+                               << result.error();
+                    StringAppendF(&cameraInfo, "Failed to stop a custom collection. %s\n",
+                                               result.error().message().c_str());
+                } else {
+                    // Pull the custom collection
+                    cameraInfo += *result;
+                }
+            } else {
+                WriteStringToFd(StringPrintf("Unknown argument: %s\n",
+                                             subcommand.c_str()),
+                                fd);
+                cmdHelp(fd);
+                return;
+            }
+        } else {
+            WriteStringToFd(StringPrintf("Unknown command: %s\n"
+                                         "Please check the usages:\n", command.c_str()),
+                            fd);
+            cmdHelp(fd);
+            return;
+        }
+
+        // Outputs the report
+        WriteStringToFd(cameraInfo, fd);
     }
 
     if (dumpDisplays) {
-        dprintf(fd, "Not implemented yet\n");
+        HalDisplay* pDisplay =
+            reinterpret_cast<HalDisplay*>(mActiveDisplay.promote().get());
+        if (!pDisplay) {
+            WriteStringToFd("No active display is found.\n", fd);
+        } else {
+            WriteStringToFd(pDisplay->toString(kSingleIndent), fd);
+        }
     }
 }
 
diff --git a/evs/manager/1.1/Enumerator.h b/evs/manager/1.1/Enumerator.h
index f23871e..7708295 100644
--- a/evs/manager/1.1/Enumerator.h
+++ b/evs/manager/1.1/Enumerator.h
@@ -17,13 +17,14 @@
 #ifndef ANDROID_AUTOMOTIVE_EVS_V1_1_EVSCAMERAENUMERATOR_H
 #define ANDROID_AUTOMOTIVE_EVS_V1_1_EVSCAMERAENUMERATOR_H
 
+#include "HalCamera.h"
+#include "VirtualCamera.h"
+#include "stats/StatsCollector.h"
+
 #include <list>
 #include <unordered_map>
 #include <unordered_set>
 
-#include "HalCamera.h"
-#include "VirtualCamera.h"
-
 #include <android/hardware/automotive/evs/1.1/IEvsEnumerator.h>
 #include <android/hardware/automotive/evs/1.1/IEvsDisplay.h>
 #include <android/hardware/camera/device/3.2/ICameraDevice.h>
@@ -93,9 +94,18 @@
     std::unordered_map<std::string,
                        CameraDesc>    mCameraDevices;
 
+    // List of available physical display devices
+    std::list<uint8_t>                mDisplayPorts;
+
     // Display port the internal display is connected to.
     uint8_t                           mInternalDisplayPort;
 
+    // Collecting camera usage statistics from clients
+    sp<StatsCollector>                mClientsMonitor;
+
+    // Boolean flag to tell whether the camera usages are being monitored or not
+    bool                              mMonitorEnabled;
+
     // LSHAL dump
     void cmdDump(int fd, const hidl_vec<hidl_string>& options);
     void cmdHelp(int fd);
diff --git a/evs/manager/1.1/HalCamera.cpp b/evs/manager/1.1/HalCamera.cpp
index 4b5feae..a90e83b 100644
--- a/evs/manager/1.1/HalCamera.cpp
+++ b/evs/manager/1.1/HalCamera.cpp
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
+#include "Enumerator.h"
 #include "HalCamera.h"
 #include "VirtualCamera.h"
-#include "Enumerator.h"
 
+#include <android-base/file.h>
 #include <android-base/logging.h>
-#include <ui/GraphicBufferAllocator.h>
-#include <ui/GraphicBufferMapper.h>
-
+#include <android-base/strings.h>
 
 namespace android {
 namespace automotive {
@@ -33,6 +32,15 @@
 // TODO(changyeon):
 // We need to hook up death monitoring to detect stream death so we can attempt a reconnect
 
+using ::android::base::StringAppendF;
+using ::android::base::WriteStringToFd;
+
+HalCamera::~HalCamera() {
+    // Reports the usage statistics before the destruction
+    // EvsUsageStatsReported atom is defined in
+    // frameworks/base/cmds/statsd/src/atoms.proto
+    mUsageStats->writeStats();
+}
 
 sp<VirtualCamera> HalCamera::makeVirtualCamera() {
 
@@ -313,6 +321,9 @@
         if (mFrames[i].refCount <= 0) {
             // Since all our clients are done with this buffer, return it to the device layer
             mHwCamera->doneWithFrame(buffer);
+
+            // Counts a returned buffer
+            mUsageStats->framesReturned();
         }
     }
 
@@ -339,6 +350,9 @@
             returnedBuffers.resize(1);
             returnedBuffers[0] = buffer;
             mHwCamera->doneWithFrame_1_1(returnedBuffers);
+
+            // Counts a returned buffer
+            mUsageStats->framesReturned();
         }
     }
 
@@ -355,6 +369,10 @@
     LOG(INFO) << "A delivered frame from EVS v1.0 HW module is rejected.";
     mHwCamera->doneWithFrame(buffer);
 
+    // Reports a received and returned buffer
+    mUsageStats->framesReceived();
+    mUsageStats->framesReturned();
+
     return Void();
 }
 
@@ -381,7 +399,9 @@
                 // Skip current frame because it arrives too soon.
                 LOG(DEBUG) << "Skips a frame from " << getId();
                 mNextRequests->push_back(req);
-                ++mSyncFrames;
+
+                // Reports a skipped frame
+                mUsageStats->framesSkippedToSync();
             } else if (vCam != nullptr && vCam->deliverFrame(buffer[0])) {
                 // Forward a frame and move a timeline.
                 LOG(DEBUG) << getId() << " forwarded the buffer #" << buffer[0].bufferId;
@@ -390,7 +410,9 @@
             }
         }
     }
-    ++mFramesReceived;
+
+    // Reports the number of received buffers
+    mUsageStats->framesReceived(buffer.size());
 
     // Frames are being forwarded to active v1.0 clients and v1.1 clients if we
     // failed to create a timeline.
@@ -412,8 +434,10 @@
         // right away.
         LOG(INFO) << "Trivially rejecting frame (" << buffer[0].bufferId
                   << ") from " << getId() << " with no acceptance";
-        ++mFramesNotUsed;
         mHwCamera->doneWithFrame_1_1(buffer);
+
+        // Reports a returned buffer
+        mUsageStats->framesReturned();
     } else {
         // Add an entry for this frame in our tracking list.
         unsigned i;
@@ -562,47 +586,68 @@
 }
 
 
-void HalCamera::dump(int fd) const {
-    dprintf(fd, "HalCamera: %s\n", mId.c_str());
-    const auto timeElapsedNano = android::elapsedRealtimeNano() - mTimeCreated;
-    dprintf(fd, "\tCreated: %ld (elapsed %ld ns)\n", (long)mTimeCreated, (long)timeElapsedNano);
-    dprintf(fd, "\tFrames received: %lu (%f fps)\n",
-                (unsigned long)mFramesReceived,
-                (double)mFramesReceived / timeElapsedNano * 1e+9);
-    dprintf(fd, "\tFrames not used: %lu\n", (unsigned long)mFramesNotUsed);
-    dprintf(fd, "\tFrames skipped to sync: %lu\n", (unsigned long)mSyncFrames);
-    dprintf(fd, "\tActive Stream Configuration:\n");
-    dprintf(fd, "\t\tid: %d\n", mStreamConfig.id);
-    dprintf(fd, "\t\twidth: %d\n", mStreamConfig.width);
-    dprintf(fd, "\t\theight: %d\n", mStreamConfig.height);
-    dprintf(fd, "\t\tformat: %d\n", mStreamConfig.width);
-    dprintf(fd, "\t\tusage: 0x%lX\n", (unsigned long)mStreamConfig.usage);
-    dprintf(fd, "\t\trotation: 0x%X\n", mStreamConfig.rotation);
+CameraUsageStatsRecord HalCamera::getStats() const {
+    return mUsageStats->snapshot();
+}
 
-    dprintf(fd, "\tActive clients:\n");
+
+Stream HalCamera::getStreamConfiguration() const {
+    return mStreamConfig;
+}
+
+
+std::string HalCamera::toString(const char* indent) const {
+    std::string buffer;
+
+    const auto timeElapsedMs = android::uptimeMillis() - mTimeCreatedMs;
+    StringAppendF(&buffer, "%sCreated: @%" PRId64 " (elapsed %" PRId64 " ms)\n",
+                           indent, mTimeCreatedMs, timeElapsedMs);
+
+    std::string double_indent(indent);
+    double_indent += indent;
+    buffer += CameraUsageStats::toString(getStats(), double_indent.c_str());
     for (auto&& client : mClients) {
         auto handle = client.promote();
         if (!handle) {
             continue;
         }
 
-        dprintf(fd, "\t\tClient %p\n", handle.get());
-        handle->dump(fd, "\t\t\t");
-        {
-            std::scoped_lock<std::mutex> lock(mFrameMutex);
-            dprintf(fd, "\t\t\tUse a fence-based delivery: %s\n",
-                    mTimelines.find((uint64_t)handle.get()) != mTimelines.end() ? "T" : "F");
-        }
+        StringAppendF(&buffer, "%sClient %p\n",
+                               indent, handle.get());
+        buffer += handle->toString(double_indent.c_str());
     }
 
-    dprintf(fd, "\tMaster client: %p\n", mMaster.promote().get());
-    dprintf(fd, "\tSynchronization support: %s\n", mSyncSupported ? "T" : "F");
+    StringAppendF(&buffer, "%sMaster client: %p\n"
+                           "%sSynchronization support: %s\n",
+                           indent, mMaster.promote().get(),
+                           indent, mSyncSupported ? "T":"F");
+
+    buffer += HalCamera::toString(mStreamConfig, indent);
+
+    return buffer;
 }
 
 
-double HalCamera::getFramerate() const {
-    const auto timeElapsed = android::elapsedRealtimeNano() - mTimeCreated;
-    return static_cast<double>(mFramesReceived) / timeElapsed;
+std::string HalCamera::toString(Stream configuration, const char* indent) {
+    std::string streamInfo;
+    std::string double_indent(indent);
+    double_indent += indent;
+    StringAppendF(&streamInfo, "%sActive Stream Configuration\n"
+                               "%sid: %d\n"
+                               "%swidth: %d\n"
+                               "%sheight: %d\n"
+                               "%sformat: 0x%X\n"
+                               "%susage: 0x%" PRIx64 "\n"
+                               "%srotation: 0x%X\n\n",
+                               indent,
+                               double_indent.c_str(), configuration.id,
+                               double_indent.c_str(), configuration.width,
+                               double_indent.c_str(), configuration.height,
+                               double_indent.c_str(), configuration.format,
+                               double_indent.c_str(), configuration.usage,
+                               double_indent.c_str(), configuration.rotation);
+
+    return streamInfo;
 }
 
 
diff --git a/evs/manager/1.1/HalCamera.h b/evs/manager/1.1/HalCamera.h
index 88f8592..7e010a6 100644
--- a/evs/manager/1.1/HalCamera.h
+++ b/evs/manager/1.1/HalCamera.h
@@ -17,21 +17,22 @@
 #ifndef ANDROID_AUTOMOTIVE_EVS_V1_1_HALCAMERA_H
 #define ANDROID_AUTOMOTIVE_EVS_V1_1_HALCAMERA_H
 
-#include <android/hardware/automotive/evs/1.1/types.h>
-#include <android/hardware/automotive/evs/1.1/IEvsCamera.h>
-#include <android/hardware/automotive/evs/1.1/IEvsCameraStream.h>
-#include <ui/GraphicBuffer.h>
-#include <utils/SystemClock.h>
-
-#include <thread>
-#include <list>
-#include <deque>
-#include <unordered_map>
-
+#include "stats/CameraUsageStats.h"
 #include "sync/unique_fd.h"
 #include "sync/unique_fence.h"
 #include "sync/unique_timeline.h"
 
+#include <deque>
+#include <list>
+#include <thread>
+#include <unordered_map>
+
+#include <android/hardware/automotive/evs/1.1/types.h>
+#include <android/hardware/automotive/evs/1.1/IEvsCamera.h>
+#include <android/hardware/automotive/evs/1.1/IEvsCameraStream.h>
+#include <utils/Mutex.h>
+#include <utils/SystemClock.h>
+
 using namespace ::android::hardware::automotive::evs::V1_1;
 using ::android::hardware::camera::device::V3_2::Stream;
 using ::android::hardware::Return;
@@ -60,19 +61,22 @@
 // stream from the hardware camera and distribute it to the associated VirtualCamera objects.
 class HalCamera : public IEvsCameraStream_1_1 {
 public:
-    HalCamera(sp<IEvsCamera_1_1> hwCamera, std::string deviceId = "", Stream cfg = {})
+    HalCamera(sp<IEvsCamera_1_1> hwCamera,
+              std::string deviceId = "",
+              int32_t recordId = 0,
+              Stream cfg = {})
         : mHwCamera(hwCamera),
           mId(deviceId),
           mStreamConfig(cfg),
           mSyncSupported(UniqueTimeline::Supported()),
-          mTimeCreated(android::elapsedRealtimeNano()),
-          mFramesReceived(0),
-          mFramesNotUsed(0),
-          mSyncFrames(0) {
+          mTimeCreatedMs(android::uptimeMillis()),
+          mUsageStats(new CameraUsageStats(recordId)) {
         mCurrentRequests = &mFrameRequests[0];
         mNextRequests    = &mFrameRequests[1];
     }
 
+    virtual ~HalCamera();
+
     // Factory methods for client VirtualCameras
     sp<VirtualCamera>     makeVirtualCamera();
     bool                  ownVirtualCamera(sp<VirtualCamera> virtualCamera);
@@ -101,8 +105,17 @@
     Return<EvsResult>   getParameter(CameraParam id, int32_t& value);
     bool                isSyncSupported() const { return mSyncSupported; }
 
-    void                dump(int fd) const;
-    double              getFramerate() const;
+    // Returns a snapshot of collected usage statistics
+    CameraUsageStatsRecord getStats() const;
+
+    // Returns active stream configuration
+    Stream getStreamConfiguration() const;
+
+    // Returns a string showing the current status
+    std::string toString(const char* indent = "") const;
+
+    // Returns a string showing current stream configuration
+    static std::string toString(Stream configuration, const char* indent = "");
 
     // Methods from ::android::hardware::automotive::evs::V1_0::IEvsCameraStream follow.
     Return<void> deliverFrame(const BufferDesc_1_0& buffer) override;
@@ -145,11 +158,11 @@
                        std::unique_ptr<UniqueTimeline>> mTimelines GUARDED_BY(mFrameMutex);
     bool                      mSyncSupported;
 
-    // debugging information
-    int64_t                   mTimeCreated;
-    uint64_t                  mFramesReceived;
-    uint64_t                  mFramesNotUsed;
-    uint64_t                  mSyncFrames;
+    // Time this object was created
+    int64_t mTimeCreatedMs;
+
+    // usage statistics to collect
+    android::sp<CameraUsageStats> mUsageStats;
 };
 
 } // namespace implementation
diff --git a/evs/manager/1.1/HalDisplay.cpp b/evs/manager/1.1/HalDisplay.cpp
index 4f7b4fd..b1e1aa6 100644
--- a/evs/manager/1.1/HalDisplay.cpp
+++ b/evs/manager/1.1/HalDisplay.cpp
@@ -14,17 +14,26 @@
  * limitations under the License.
  */
 
-#include <android-base/logging.h>
 #include "HalDisplay.h"
 
+#include <inttypes.h>
+
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <ui/DisplayConfig.h>
+#include <ui/DisplayState.h>
+
+using android::base::StringAppendF;
+
 namespace android {
 namespace automotive {
 namespace evs {
 namespace V1_1 {
 namespace implementation {
 
-HalDisplay::HalDisplay(sp<IEvsDisplay_1_0> display) :
-  mHwDisplay(display) {
+HalDisplay::HalDisplay(sp<IEvsDisplay_1_0> display, int32_t id) :
+  mHwDisplay(display),
+  mId(id) {
     // nothing to do.
 }
 
@@ -114,6 +123,38 @@
     return Void();
 }
 
+
+std::string HalDisplay::toString(const char* indent) {
+    std::string buffer;
+    android::DisplayConfig displayConfig;
+    android::ui::DisplayState displayState;
+
+    if (mId == std::numeric_limits<int32_t>::min()) {
+        // Display identifier has not set
+        StringAppendF(&buffer, "HalDisplay: Display port is unknown.\n");
+    } else {
+        StringAppendF(&buffer, "HalDisplay: Display port %" PRId32 "\n", mId);
+    }
+
+    getDisplayInfo_1_1([&](auto& config, auto& state) {
+        displayConfig =
+            *(reinterpret_cast<const android::DisplayConfig*>(config.data()));
+        displayState =
+            *(reinterpret_cast<const android::ui::DisplayState*>(state.data()));
+    });
+
+    StringAppendF(&buffer, "%sWidth: %" PRId32 "\n",
+                           indent, displayConfig.resolution.getWidth());
+    StringAppendF(&buffer, "%sHeight: %" PRId32 "\n",
+                           indent, displayConfig.resolution.getHeight());
+    StringAppendF(&buffer, "%sRefresh rate: %f\n",
+                           indent, displayConfig.refreshRate);
+    StringAppendF(&buffer, "%sRotation: %" PRId32 "\n",
+                           indent, static_cast<int32_t>(displayState.orientation));
+
+    return buffer;
+}
+
 } // namespace implementation
 } // namespace V1_1
 } // namespace evs
diff --git a/evs/manager/1.1/HalDisplay.h b/evs/manager/1.1/HalDisplay.h
index de9690b..672ad55 100644
--- a/evs/manager/1.1/HalDisplay.h
+++ b/evs/manager/1.1/HalDisplay.h
@@ -17,6 +17,8 @@
 #ifndef ANDROID_AUTOMOTIVE_EVS_V1_1_DISPLAYPROXY_H
 #define ANDROID_AUTOMOTIVE_EVS_V1_1_DISPLAYPROXY_H
 
+#include <limits>
+
 #include <android/hardware/automotive/evs/1.1/types.h>
 #include <android/hardware/automotive/evs/1.1/IEvsDisplay.h>
 
@@ -42,7 +44,8 @@
 // manager directly to use the IEvsDisplay object the driver provides.
 class HalDisplay : public IEvsDisplay_1_1 {
 public:
-    explicit HalDisplay(sp<IEvsDisplay_1_0> display);
+    explicit HalDisplay(sp<IEvsDisplay_1_0> display,
+                        int32_t port = std::numeric_limits<int32_t>::min());
     virtual ~HalDisplay() override;
 
     inline void         shutdown();
@@ -58,8 +61,12 @@
     // Methods from ::android::hardware::automotive::evs::V1_1::IEvsDisplay follow.
     Return<void>            getDisplayInfo_1_1(getDisplayInfo_1_1_cb _info_cb) override;
 
+    // Returns a string showing the current status
+    std::string toString(const char* indent = "");
+
 private:
     sp<IEvsDisplay_1_0>     mHwDisplay; // The low level display interface that backs this proxy
+    int32_t                 mId; // Display identifier
 };
 
 } // namespace implementation
diff --git a/evs/manager/1.1/VirtualCamera.cpp b/evs/manager/1.1/VirtualCamera.cpp
index cfef6ea..8b94a15 100644
--- a/evs/manager/1.1/VirtualCamera.cpp
+++ b/evs/manager/1.1/VirtualCamera.cpp
@@ -18,10 +18,14 @@
 #include "HalCamera.h"
 #include "Enumerator.h"
 
+#include <android/hardware_buffer.h>
+#include <android-base/file.h>
 #include <android-base/logging.h>
-#include <ui/GraphicBufferAllocator.h>
-#include <ui/GraphicBufferMapper.h>
+#include <android-base/stringprintf.h>
 
+using ::android::base::StringAppendF;
+using ::android::base::StringPrintf;
+using ::android::base::WriteStringToFd;
 using ::android::hardware::automotive::evs::V1_0::DisplayState;
 
 
@@ -894,15 +898,27 @@
 }
 
 
-void VirtualCamera::dump(int fd, const char* prefix) const {
-    dprintf(fd, "%sLogical camera device: %s\n",
-                prefix, mHalCamera.size() > 1 ? "T" : "F");
-    dprintf(fd, "%sFramesAllowed: %u\n", prefix, mFramesAllowed);
-    dprintf(fd, "%sFrames in use:\n", prefix);
+std::string VirtualCamera::toString(const char* indent) const {
+    std::string buffer;
+    StringAppendF(&buffer, "%sLogical camera device: %s\n"
+                           "%sFramesAllowed: %u\n"
+                           "%sFrames in use:\n",
+                           indent, mHalCamera.size() > 1 ? "T" : "F",
+                           indent, mFramesAllowed,
+                           indent);
+
+    std::string next_indent(indent);
+    next_indent += "\t";
     for (auto&& [id, queue] : mFramesHeld) {
-        dprintf(fd, "%s\t%s, %d\n", prefix, id.c_str(), (int)queue.size());
+        StringAppendF(&buffer, "%s%s: %d\n",
+                               next_indent.c_str(),
+                               id.c_str(),
+                               static_cast<int>(queue.size()));
     }
-    dprintf(fd, "%sCurrent stream state: %d\n", prefix, mStreamState);
+    StringAppendF(&buffer, "%sCurrent stream state: %d\n",
+                                 indent, mStreamState);
+
+    return buffer;
 }
 
 
diff --git a/evs/manager/1.1/VirtualCamera.h b/evs/manager/1.1/VirtualCamera.h
index 2a8eae9..76a8648 100644
--- a/evs/manager/1.1/VirtualCamera.h
+++ b/evs/manager/1.1/VirtualCamera.h
@@ -21,7 +21,6 @@
 #include <android/hardware/automotive/evs/1.1/IEvsCamera.h>
 #include <android/hardware/automotive/evs/1.1/IEvsCameraStream.h>
 #include <android/hardware/automotive/evs/1.1/IEvsDisplay.h>
-#include <ui/GraphicBuffer.h>
 
 #include <thread>
 #include <deque>
@@ -109,7 +108,7 @@
                                             importExternalBuffers_cb _hidl_cb) override;
 
     // Dump current status to a given file descriptor
-    void              dump(int fd, const char* prefix = "") const;
+    std::string       toString(const char* indent = "") const;
 
 
 private:
diff --git a/evs/manager/1.1/stats/CameraUsageStats.cpp b/evs/manager/1.1/stats/CameraUsageStats.cpp
new file mode 100644
index 0000000..11f8229
--- /dev/null
+++ b/evs/manager/1.1/stats/CameraUsageStats.cpp
@@ -0,0 +1,113 @@
+/*
+ * 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 "CameraUsageStats.h"
+
+#include <statslog.h>
+
+namespace android {
+namespace automotive {
+namespace evs {
+namespace V1_1 {
+namespace implementation {
+
+using ::android::base::Result;
+using ::android::base::StringAppendF;
+
+
+void CameraUsageStats::framesReceived(int n) {
+    AutoMutex lock(mMutex);
+    mStats.framesReceived += n;
+}
+
+
+void CameraUsageStats::framesReturned(int n) {
+    AutoMutex lock(mMutex);
+    mStats.framesReturned += n;
+}
+
+
+void CameraUsageStats::framesIgnored(int n) {
+    AutoMutex lock(mMutex);
+    mStats.framesIgnored += n;
+}
+
+
+void CameraUsageStats::framesSkippedToSync(int n) {
+    AutoMutex lock(mMutex);
+    mStats.framesSkippedToSync += n;
+}
+
+
+void CameraUsageStats::eventsReceived() {
+    AutoMutex lock(mMutex);
+    ++mStats.erroneousEventsCount;
+}
+
+
+int64_t CameraUsageStats::getTimeCreated() const {
+    AutoMutex lock(mMutex);
+    return mTimeCreatedMs;
+}
+
+
+int64_t CameraUsageStats::getFramesReceived() const {
+    AutoMutex lock(mMutex);
+    return mStats.framesReceived;
+}
+
+
+int64_t CameraUsageStats::getFramesReturned() const {
+    AutoMutex lock(mMutex);
+    return mStats.framesReturned;
+}
+
+
+CameraUsageStatsRecord CameraUsageStats::snapshot() const {
+    AutoMutex lock(mMutex);
+    return mStats;
+}
+
+
+Result<void> CameraUsageStats::writeStats() const {
+    AutoMutex lock(mMutex);
+    const auto duration = android::uptimeMillis() - mTimeCreatedMs;
+    // TODO(b/156131016): calculates and reports frame roundtrip latencies
+    android::util::stats_write(android::util::EVS_USAGE_STATS_REPORTED,
+                               mId,
+                               mStats.peakClientsCount,
+                               mStats.erroneousEventsCount,
+                               mStats.framesFirstRoundtripLatency,
+                               mStats.framesAvgRoundtripLatency,
+                               mStats.framesPeakRoundtripLatency,
+                               mStats.framesReceived,
+                               mStats.framesIgnored,
+                               mStats.framesSkippedToSync,
+                               duration);
+    return {};
+}
+
+
+std::string CameraUsageStats::toString(const CameraUsageStatsRecord& record, const char* indent) {
+    return record.toString(indent);
+}
+
+} // namespace implementation
+} // namespace V1_1
+} // namespace evs
+} // namespace automotive
+} // namespace android
+
diff --git a/evs/manager/1.1/stats/CameraUsageStats.h b/evs/manager/1.1/stats/CameraUsageStats.h
new file mode 100644
index 0000000..4bc4ea8
--- /dev/null
+++ b/evs/manager/1.1/stats/CameraUsageStats.h
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+#ifndef ANDROID_AUTOMOTIVE_EVS_V1_1_CAMERAUSAGESTATS_H
+#define ANDROID_AUTOMOTIVE_EVS_V1_1_CAMERAUSAGESTATS_H
+
+#include <inttypes.h>
+
+#include <android-base/result.h>
+#include <android-base/stringprintf.h>
+#include <utils/Mutex.h>
+#include <utils/RefBase.h>
+#include <utils/SystemClock.h>
+
+namespace android {
+namespace automotive {
+namespace evs {
+namespace V1_1 {
+namespace implementation {
+
+struct CameraUsageStatsRecord {
+public:
+    // Time a snapshot is generated
+    nsecs_t timestamp;
+
+    // Total number of frames received
+    int64_t framesReceived;
+
+    // Total number of frames returned to EVS HAL
+    int64_t framesReturned;
+
+    // Number of frames ignored because no clients are listening
+    int64_t framesIgnored;
+
+    // Number of frames skipped to synchronize camera frames
+    int64_t framesSkippedToSync;
+
+    // Roundtrip latency of the very first frame after the stream started.
+    int64_t framesFirstRoundtripLatency;
+
+    // Peak mFrame roundtrip latency
+    int64_t framesPeakRoundtripLatency;
+
+    // Average mFrame roundtrip latency
+    double  framesAvgRoundtripLatency;
+
+    // Number of the erroneous streaming events
+    int32_t erroneousEventsCount;
+
+    // Peak number of active clients
+    int32_t peakClientsCount;
+
+    // Calculates a delta between two records
+    CameraUsageStatsRecord& operator-=(const CameraUsageStatsRecord& rhs) {
+        // Only calculates differences in the frame statistics
+        framesReceived = framesReceived - rhs.framesReceived;
+        framesReturned = framesReturned - rhs.framesReturned;
+        framesIgnored = framesIgnored - rhs.framesIgnored;
+        framesSkippedToSync = framesSkippedToSync - rhs.framesSkippedToSync;
+        erroneousEventsCount = erroneousEventsCount - rhs.erroneousEventsCount;
+
+        return *this;
+    }
+
+    friend CameraUsageStatsRecord operator-(CameraUsageStatsRecord lhs,
+                                      const CameraUsageStatsRecord& rhs) noexcept {
+        lhs -= rhs; // reuse compound assignment
+        return lhs;
+    }
+
+    // Constructs a string that shows collected statistics
+    std::string toString(const char* indent = "") const {
+        std::string buffer;
+        android::base::StringAppendF(&buffer,
+                "%sTime Collected: @%" PRId64 "ms\n"
+                "%sFrames Received: %" PRId64 "\n"
+                "%sFrames Returned: %" PRId64 "\n"
+                "%sFrames Ignored : %" PRId64 "\n"
+                "%sFrames Skipped To Sync: %" PRId64 "\n\n",
+                indent, ns2ms(timestamp),
+                indent, framesReceived,
+                indent, framesReturned,
+                indent, framesIgnored,
+                indent, framesSkippedToSync);
+
+        return buffer;
+    }
+};
+
+
+class CameraUsageStats : public RefBase {
+public:
+    CameraUsageStats(int32_t id)
+        : mMutex(Mutex()),
+          mId(id),
+          mTimeCreatedMs(android::uptimeMillis()),
+          mStats({}) {}
+
+private:
+    // Mutex to protect a collection record
+    mutable Mutex mMutex;
+
+    // Unique identifier
+    int32_t mId;
+
+    // Time this object was created
+    int64_t mTimeCreatedMs;
+
+    // Usage statistics to collect
+    CameraUsageStatsRecord mStats GUARDED_BY(mMutex);
+
+public:
+    void framesReceived(int n = 1) EXCLUDES(mMutex);
+    void framesReturned(int n = 1) EXCLUDES(mMutex);
+    void framesIgnored(int n = 1) EXCLUDES(mMutex);
+    void framesSkippedToSync(int n = 1) EXCLUDES(mMutex);
+    void eventsReceived() EXCLUDES(mMutex);
+    int64_t getTimeCreated() const EXCLUDES(mMutex);
+    int64_t getFramesReceived() const EXCLUDES(mMutex);
+    int64_t getFramesReturned() const EXCLUDES(mMutex);
+
+    // Returns the statistics collected so far
+    CameraUsageStatsRecord snapshot() const EXCLUDES(mMutex);
+
+    // Reports the usage statistics
+    android::base::Result<void> writeStats() const EXCLUDES(mMutex);
+
+    // Generates a string with current statistics
+    static std::string toString(const CameraUsageStatsRecord& record, const char* indent = "");
+};
+
+
+} // namespace implementation
+} // namespace V1_1
+} // namespace evs
+} // namespace automotive
+} // namespace android
+
+#endif // ANDROID_AUTOMOTIVE_EVS_V1_1_CAMERAUSAGESTATS_H
diff --git a/evs/manager/1.1/stats/LooperWrapper.cpp b/evs/manager/1.1/stats/LooperWrapper.cpp
new file mode 100644
index 0000000..041a723
--- /dev/null
+++ b/evs/manager/1.1/stats/LooperWrapper.cpp
@@ -0,0 +1,82 @@
+/**
+ * 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 "LooperWrapper.h"
+
+#include <android-base/logging.h>
+
+namespace android {
+namespace automotive {
+namespace evs {
+namespace V1_1 {
+namespace implementation {
+
+using android::sp;
+
+void LooperWrapper::wake() {
+    if (mLooper == nullptr) {
+        LOG(WARNING) << __FUNCTION__ << ": Looper is invalid.";
+        return;
+    }
+
+    return mLooper->wake();
+}
+
+int LooperWrapper::pollAll(int timeoutMillis) {
+    if (mLooper == nullptr) {
+        LOG(WARNING) << __FUNCTION__ << ": Looper is invalid.";
+        return 0;
+    }
+
+    return mLooper->pollAll(timeoutMillis);
+}
+
+void LooperWrapper::sendMessage(const sp<MessageHandler>& handler,
+                                   const Message& message) {
+    if (mLooper == nullptr) {
+        LOG(WARNING) << __FUNCTION__ << ": Looper is invalid.";
+        return;
+    }
+
+    return mLooper->sendMessage(handler, message);
+}
+
+void LooperWrapper::sendMessageAtTime(nsecs_t uptime,
+                                         const sp<MessageHandler>& handler,
+                                         const Message& message) {
+    if (mLooper == nullptr) {
+        LOG(WARNING) << __FUNCTION__ << ": Looper is invalid.";
+        return;
+    }
+
+    return mLooper->sendMessageAtTime(uptime, handler, message);
+}
+
+void LooperWrapper::removeMessages(const sp<MessageHandler>& handler) {
+    if (mLooper == nullptr) {
+        LOG(WARNING) << __FUNCTION__ << ": Looper is invalid.";
+        return;
+    }
+
+    return mLooper->removeMessages(handler);
+}
+
+} // namespace implementation
+} // namespace V1_1
+} // namespace evs
+} // namespace automotive
+} // namespace android
+
diff --git a/evs/manager/1.1/stats/LooperWrapper.h b/evs/manager/1.1/stats/LooperWrapper.h
new file mode 100644
index 0000000..fb9ec12
--- /dev/null
+++ b/evs/manager/1.1/stats/LooperWrapper.h
@@ -0,0 +1,57 @@
+/**
+ * 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.
+ */
+
+#ifndef ANDROID_AUTMOTIVE_EVS_V1_1_EVSLOOPERWRAPPER_H_
+#define ANDROID_AUTMOTIVE_EVS_V1_1_EVSLOOPERWRAPPER_H_
+
+#include <utils/Looper.h>
+#include <utils/RefBase.h>
+#include <utils/Timers.h>
+
+namespace android {
+namespace automotive {
+namespace evs {
+namespace V1_1 {
+namespace implementation {
+
+// This class wraps around android::Looper methods.  Please refer to
+// utils/Looper.h for the details.
+class LooperWrapper : public RefBase {
+public:
+    LooperWrapper() : mLooper(nullptr) {}
+    virtual ~LooperWrapper() {}
+
+    void setLooper(android::sp<Looper> looper) { mLooper = looper; }
+    void wake();
+    virtual nsecs_t now() { return systemTime(SYSTEM_TIME_MONOTONIC); }
+    virtual int pollAll(int timeoutMillis);
+    virtual void sendMessage(const android::sp<MessageHandler>& handler, const Message& message);
+    virtual void sendMessageAtTime(nsecs_t uptime, const android::sp<MessageHandler>& handler,
+                                   const Message& message);
+    virtual void removeMessages(const android::sp<MessageHandler>& handler);
+
+protected:
+    android::sp<Looper> mLooper;
+};
+
+} // namespace implementation
+} // namespace V1_1
+} // namespace evs
+} // namespace automotive
+} // namespace android
+
+#endif  // ANDROID_AUTMOTIVE_EVS_V1_1_EVSLOOPERWRAPPER_H_
+
diff --git a/evs/manager/1.1/stats/StatsCollector.cpp b/evs/manager/1.1/stats/StatsCollector.cpp
new file mode 100644
index 0000000..de85e5a
--- /dev/null
+++ b/evs/manager/1.1/stats/StatsCollector.cpp
@@ -0,0 +1,468 @@
+/*
+ * 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 "HalCamera.h"
+#include "StatsCollector.h"
+#include "VirtualCamera.h"
+
+#include <processgroup/sched_policy.h>
+#include <pthread.h>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <android-base/stringprintf.h>
+#include <utils/SystemClock.h>
+
+namespace {
+
+    const char* kSingleIndent = "\t";
+    const char* kDoubleIndent = "\t\t";
+    const char* kDumpAllDevices = "all";
+
+}
+
+namespace android {
+namespace automotive {
+namespace evs {
+namespace V1_1 {
+namespace implementation {
+
+using android::base::Error;
+using android::base::EqualsIgnoreCase;
+using android::base::Result;
+using android::base::StringAppendF;
+using android::base::StringPrintf;
+using android::base::WriteStringToFd;
+using android::hardware::automotive::evs::V1_1::BufferDesc;
+
+namespace {
+
+const auto kPeriodicCollectionInterval = 10s;
+const auto kPeriodicCollectionCacheSize = 180;
+const auto kMinCollectionInterval = 1s;
+const auto kCustomCollectionMaxDuration = 30min;
+const auto kMaxDumpHistory = 10;
+
+}
+
+void StatsCollector::handleMessage(const Message& message) {
+    const auto received = static_cast<CollectionEvent>(message.what);
+    Result<void> ret;
+    switch (received) {
+        case CollectionEvent::PERIODIC:
+            ret = handleCollectionEvent(received, &mPeriodicCollectionInfo);
+            break;
+
+        case CollectionEvent::CUSTOM_START:
+            ret = handleCollectionEvent(received, &mCustomCollectionInfo);
+            break;
+
+        case CollectionEvent::CUSTOM_END: {
+            AutoMutex lock(mMutex);
+            if (mCurrentCollectionEvent != CollectionEvent::CUSTOM_START) {
+                LOG(WARNING) << "Ignoring a message to end custom collection "
+                             << "as current collection is " << toString(mCurrentCollectionEvent);
+                return;
+            }
+
+            // Starts a periodic collection
+            mLooper->removeMessages(this);
+            mCurrentCollectionEvent = CollectionEvent::PERIODIC;
+            mPeriodicCollectionInfo.lastCollectionTime = mLooper->now();
+            mLooper->sendMessage(this, CollectionEvent::PERIODIC);
+            return;
+        }
+
+        default:
+            LOG(WARNING) << "Unknown event is received: " << received;
+            break;
+    }
+
+    if (!ret.ok()) {
+        Mutex::Autolock lock(mMutex);
+        LOG(ERROR) << "Terminating data collection: "
+                   << ret.error();
+
+        mCurrentCollectionEvent = CollectionEvent::TERMINATED;
+        mLooper->removeMessages(this);
+        mLooper->wake();
+    }
+}
+
+
+Result<void> StatsCollector::handleCollectionEvent(CollectionEvent event,
+                                                   CollectionInfo* info) {
+    AutoMutex lock(mMutex);
+    if (mCurrentCollectionEvent != event) {
+        LOG(WARNING) << "Skipping " << toString(event) << " collection event "
+                     << "on collection event " << toString(mCurrentCollectionEvent);
+        return {};
+    }
+
+    if (info->maxCacheSize < 1) {
+        return Error() << "Maximum cache size must be greater than 0";
+    }
+
+    using std::chrono::duration_cast;
+    using std::chrono::seconds;
+    if (info->interval < kMinCollectionInterval) {
+        LOG(WARNING) << "Collection interval of "
+                     << duration_cast<seconds>(info->interval).count()
+                     << " seconds for " << toString(event)
+                     << " collection cannot be shorter than "
+                     << duration_cast<seconds>(kMinCollectionInterval).count()
+                     << " seconds.";
+        info->interval = kMinCollectionInterval;
+    }
+
+    auto ret = collectLocked(info);
+    if (!ret) {
+        return Error() << toString(event) << " collection failed: "
+                       << ret.error();
+    }
+
+    // Arms a message for next periodic collection
+    info->lastCollectionTime += info->interval.count();
+    mLooper->sendMessageAtTime(info->lastCollectionTime, this, event);
+    return {};
+}
+
+
+Result<void> StatsCollector::collectLocked(CollectionInfo* info) REQUIRES(mMutex) {
+    for (auto&& [id, ptr] : mClientsToMonitor) {
+        auto pClient = ptr.promote();
+        if (!pClient) {
+            LOG(DEBUG) << id << " seems not alive.";
+            continue;
+        }
+
+        // Pulls a snapshot and puts a timestamp
+        auto snapshot = pClient->getStats();
+        snapshot.timestamp = mLooper->now();
+
+        // Removes the oldest record if cache is full
+        if (info->records[id].history.size() > info->maxCacheSize) {
+            info->records[id].history.pop_front();
+        }
+
+        // Stores the latest record and the deltas
+        auto delta = snapshot - info->records[id].latest;
+        info->records[id].history.emplace_back(delta);
+        info->records[id].latest = snapshot;
+    }
+
+    return {};
+}
+
+
+Result<void> StatsCollector::startCollection() {
+    {
+        AutoMutex lock(mMutex);
+        if (mCurrentCollectionEvent != CollectionEvent::INIT ||
+            mCollectionThread.joinable()) {
+            return Error(INVALID_OPERATION)
+                   << "Camera usages collection is already running.";
+        }
+
+        // Create the collection info w/ the default values
+        mPeriodicCollectionInfo = {
+            .interval = kPeriodicCollectionInterval,
+            .maxCacheSize = kPeriodicCollectionCacheSize,
+            .lastCollectionTime = 0,
+        };
+
+    }
+
+    // Starts a background worker thread
+    mCollectionThread = std::thread([&]() {
+        {
+            AutoMutex lock(mMutex);
+            if (mCurrentCollectionEvent != CollectionEvent::INIT) {
+                LOG(ERROR) << "Skipping the statistics collection because "
+                           << "the current collection event is "
+                           << toString(mCurrentCollectionEvent);
+                return;
+            }
+
+            // Staring with a periodic collection
+            mCurrentCollectionEvent = CollectionEvent::PERIODIC;
+        }
+
+        if (set_sched_policy(0, SP_BACKGROUND) != 0) {
+            PLOG(WARNING) << "Failed to set background scheduling prioirty";
+        }
+
+        auto ret = pthread_setname_np(pthread_self(), "EvsCameraUsageCollect");
+        if (ret != 0) {
+            PLOG(WARNING) << "Failed to name a collection thread";
+        }
+
+        // Sets a looper for the communication
+        mLooper->setLooper(Looper::prepare(/*opts=*/0));
+
+        // Starts collecting the usage statistics periodically
+        mLooper->sendMessage(this, CollectionEvent::PERIODIC);
+
+        // Polls the messages until the collection is stopped.
+        bool isActive = true;
+        while (isActive) {
+            mLooper->pollAll(/*timeoutMillis=*/-1);
+            {
+                AutoMutex lock(mMutex);
+                isActive = mCurrentCollectionEvent != CollectionEvent::TERMINATED;
+            }
+        }
+    });
+
+    return {};
+}
+
+
+Result<void> StatsCollector::stopCollection() {
+    {
+        AutoMutex lock(mMutex);
+        if (mCurrentCollectionEvent == CollectionEvent::TERMINATED) {
+            LOG(WARNING) << "Camera usage data collection was stopped already.";
+            return {};
+        }
+
+        LOG(INFO) << "Stopping a camera usage data collection";
+        mCurrentCollectionEvent = CollectionEvent::TERMINATED;
+    }
+
+    // Join a background thread
+    if (mCollectionThread.joinable()) {
+        mLooper->removeMessages(this);
+        mLooper->wake();
+        mCollectionThread.join();
+    }
+
+    return {};
+}
+
+
+Result<void> StatsCollector::startCustomCollection(
+        std::chrono::nanoseconds interval,
+        std::chrono::nanoseconds maxDuration) {
+    using std::chrono::duration_cast;
+    using std::chrono::milliseconds;
+    if (interval < kMinCollectionInterval || maxDuration < kMinCollectionInterval) {
+        return Error(INVALID_OPERATION)
+                << "Collection interval and maximum maxDuration must be >= "
+                << duration_cast<milliseconds>(kMinCollectionInterval).count()
+                << " milliseconds.";
+    }
+
+    if (maxDuration > kCustomCollectionMaxDuration) {
+        return Error(INVALID_OPERATION)
+                << "Collection maximum maxDuration must be less than "
+                << duration_cast<milliseconds>(kCustomCollectionMaxDuration).count()
+                << " milliseconds.";
+    }
+
+    {
+        AutoMutex lock(mMutex);
+        if (mCurrentCollectionEvent != CollectionEvent::PERIODIC) {
+            return Error(INVALID_OPERATION)
+                    << "Cannot start a custom collection when "
+                    << "the current collection event " << toString(mCurrentCollectionEvent)
+                    << " != " << toString(CollectionEvent::PERIODIC) << " collection event";
+        }
+
+        // Notifies the user if a preview custom collection result is
+        // not used yet.
+        if (mCustomCollectionInfo.records.size() > 0) {
+            LOG(WARNING) << "Previous custom collection result, which was done at "
+                         << mCustomCollectionInfo.lastCollectionTime
+                         << " has not pulled yet will be overwritten.";
+        }
+
+        // Programs custom collection configurations
+        mCustomCollectionInfo = {
+                .interval = interval,
+                .maxCacheSize = std::numeric_limits<std::size_t>::max(),
+                .lastCollectionTime = mLooper->now(),
+                .records = {},
+        };
+
+        mLooper->removeMessages(this);
+        nsecs_t uptime = mLooper->now() + maxDuration.count();
+        mLooper->sendMessageAtTime(uptime, this, CollectionEvent::CUSTOM_END);
+        mCurrentCollectionEvent = CollectionEvent::CUSTOM_START;
+        mLooper->sendMessage(this, CollectionEvent::CUSTOM_START);
+    }
+
+    return {};
+}
+
+
+Result<std::string> StatsCollector::stopCustomCollection(std::string targetId) {
+    Mutex::Autolock lock(mMutex);
+    if (mCurrentCollectionEvent == CollectionEvent::CUSTOM_START) {
+        // Stops a running custom collection
+        mLooper->removeMessages(this);
+        mLooper->sendMessage(this, CollectionEvent::CUSTOM_END);
+    }
+
+    auto ret = collectLocked(&mCustomCollectionInfo);
+    if (!ret) {
+        return Error() << toString(mCurrentCollectionEvent) << " collection failed: "
+                       << ret.error();
+    }
+
+    // Prints out the all collected statistics
+    std::string buffer;
+    using std::chrono::duration_cast;
+    using std::chrono::seconds;
+    const intmax_t interval =
+        duration_cast<seconds>(mCustomCollectionInfo.interval).count();
+    if (EqualsIgnoreCase(targetId, kDumpAllDevices)) {
+        for (auto& [id, records] : mCustomCollectionInfo.records) {
+
+            StringAppendF(&buffer, "%s\n"
+                                   "%sNumber of collections: %zu\n"
+                                   "%sCollection interval: %" PRIdMAX " secs\n",
+                                   id.c_str(),
+                                   kSingleIndent, records.history.size(),
+                                   kSingleIndent, interval);
+            auto it = records.history.rbegin();
+            while (it != records.history.rend()) {
+                buffer += it++->toString(kDoubleIndent);
+            }
+        }
+
+        // Clears the collection
+        mCustomCollectionInfo = {};
+    } else {
+        auto it = mCustomCollectionInfo.records.find(targetId);
+        if (it != mCustomCollectionInfo.records.end()) {
+            StringAppendF(&buffer, "%s\n"
+                                   "%sNumber of collections: %zu\n"
+                                   "%sCollection interval: %" PRIdMAX " secs\n",
+                                   targetId.c_str(),
+                                   kSingleIndent, it->second.history.size(),
+                                   kSingleIndent, interval);
+            auto recordIter = it->second.history.rbegin();
+            while (recordIter != it->second.history.rend()) {
+                buffer += recordIter++->toString(kDoubleIndent);
+            }
+
+            // Clears the collection
+            mCustomCollectionInfo = {};
+        } else {
+            // Keeps the collection as the users may want to execute a command
+            // again with a right device id
+            StringAppendF(&buffer, "%s has not been monitored.", targetId.c_str());
+        }
+    }
+
+    return buffer;
+}
+
+
+Result<void> StatsCollector::registerClientToMonitor(android::sp<HalCamera>& camera) {
+    if (!camera) {
+        return Error(BAD_VALUE) << "Given camera client is invalid";
+    }
+
+    AutoMutex lock(mMutex);
+    const auto id = camera->getId();
+    if (mClientsToMonitor.find(id) != mClientsToMonitor.end()) {
+        LOG(WARNING) << id << " is already registered.";
+    } else {
+        mClientsToMonitor.insert_or_assign(id, camera);
+    }
+
+    return {};
+}
+
+
+Result<void> StatsCollector::unregisterClientToMonitor(const std::string& id) {
+    AutoMutex lock(mMutex);
+    auto entry = mClientsToMonitor.find(id);
+    if (entry != mClientsToMonitor.end()) {
+        mClientsToMonitor.erase(entry);
+    } else {
+        LOG(WARNING) << id << " has not been registerd.";
+    }
+
+    return {};
+}
+
+
+std::string StatsCollector::toString(const CollectionEvent& event) const {
+    switch(event) {
+        case CollectionEvent::INIT:
+            return "CollectionEvent::INIT";
+        case CollectionEvent::PERIODIC:
+            return "CollectionEvent::PERIODIC";
+        case CollectionEvent::CUSTOM_START:
+            return "CollectionEvent::CUSTOM_START";
+        case CollectionEvent::CUSTOM_END:
+            return "CollectionEvent::CUSTOM_END";
+        case CollectionEvent::TERMINATED:
+            return "CollectionEvent::TERMINATED";
+
+        default:
+            return "Unknown";
+    }
+}
+
+
+Result<void> StatsCollector::toString(std::unordered_map<std::string, std::string>* usages,
+                                      const char* indent) EXCLUDES(mMutex) {
+    std::string double_indent(indent);
+    double_indent += indent;
+
+    {
+        AutoMutex lock(mMutex);
+        using std::chrono::duration_cast;
+        using std::chrono::seconds;
+        const intmax_t interval =
+            duration_cast<seconds>(mPeriodicCollectionInfo.interval).count();
+
+        for (auto&& [id, records] : mPeriodicCollectionInfo.records) {
+            std::string buffer;
+            StringAppendF(&buffer, "%s\n"
+                                   "%sNumber of collections: %zu\n"
+                                   "%sCollection interval: %" PRIdMAX "secs\n",
+                                   id.c_str(),
+                                   indent, records.history.size(),
+                                   indent, interval);
+
+            // Adding up to kMaxDumpHistory records
+            auto it = records.history.rbegin();
+            auto count = 0;
+            while (it != records.history.rend() && count < kMaxDumpHistory) {
+                buffer += it->toString(double_indent.c_str());
+                ++it;
+                ++count;
+            }
+
+            usages->insert_or_assign(id, std::move(buffer));
+        }
+    }
+
+    return {};
+}
+
+
+} // namespace implementation
+} // namespace V1_1
+} // namespace evs
+} // namespace automotive
+} // namespace android
+
diff --git a/evs/manager/1.1/stats/StatsCollector.h b/evs/manager/1.1/stats/StatsCollector.h
new file mode 100644
index 0000000..031b342
--- /dev/null
+++ b/evs/manager/1.1/stats/StatsCollector.h
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+
+#ifndef ANDROID_AUTOMOTIVE_EVS_V1_1_EVSSTATSCOLLECTOR_H
+#define ANDROID_AUTOMOTIVE_EVS_V1_1_EVSSTATSCOLLECTOR_H
+
+#include "CameraUsageStats.h"
+#include "LooperWrapper.h"
+
+#include <deque>
+#include <thread>
+#include <unordered_map>
+#include <vector>
+
+#include <android/hardware/automotive/evs/1.1/types.h>
+#include <android-base/chrono_utils.h>
+#include <android-base/logging.h>
+#include <android-base/result.h>
+#include <utils/Mutex.h>
+
+namespace android {
+namespace automotive {
+namespace evs {
+namespace V1_1 {
+namespace implementation {
+
+class HalCamera;    // From VirtualCamera.h
+
+enum CollectionEvent {
+    INIT = 0,
+    PERIODIC,
+    CUSTOM_START,
+    CUSTOM_END,
+    TERMINATED,
+
+    LAST_EVENT,
+};
+
+
+struct CollectionRecord {
+    // Latest statistics collection
+    CameraUsageStatsRecord latest = {};
+
+    // History of collected statistics records
+    std::deque<CameraUsageStatsRecord> history;
+};
+
+
+struct CollectionInfo {
+    // Collection interval between two subsequent collections
+    std::chrono::nanoseconds interval = 0ns;
+
+    // The maximum number of records this collection stores
+    size_t maxCacheSize = 0;
+
+    // Time when the latest collection was done
+    nsecs_t lastCollectionTime = 0;
+
+    // Collected statistics records per instances
+    std::unordered_map<std::string, CollectionRecord> records;
+};
+
+
+class StatsCollector : public MessageHandler {
+public:
+    explicit StatsCollector() :
+        mLooper(new LooperWrapper()),
+        mCurrentCollectionEvent(CollectionEvent::INIT),
+        mPeriodicCollectionInfo({}),
+        mCustomCollectionInfo({}) {}
+
+    virtual ~StatsCollector() { stopCollection(); }
+
+    // Starts collecting CameraUsageStats
+    android::base::Result<void> startCollection();
+
+    // Stops collecting the statistics
+    android::base::Result<void> stopCollection();
+
+    // Starts collecting CameraUsageStarts during a given duration at a given
+    // interval.
+    android::base::Result<void> startCustomCollection(
+            std::chrono::nanoseconds interval,
+            std::chrono::nanoseconds duration) EXCLUDES(mMutex);
+
+    // Stops current custom collection and shows the result from the device with
+    // a given unique id.  If this is "all",all results
+    // will be returned.
+    android::base::Result<std::string> stopCustomCollection(
+            std::string id = "") EXCLUDES(mMutex);
+
+    // Registers HalCamera object to monitor
+    android::base::Result<void> registerClientToMonitor(
+            android::sp<HalCamera>& camera) EXCLUDES(mMutex);
+
+    // Unregister HalCamera object
+    android::base::Result<void> unregisterClientToMonitor(
+            const std::string& id) EXCLUDES(mMutex);
+
+    // Returns a string that contains the latest statistics pulled from
+    // currently active clients
+    android::base::Result<void> toString(
+            std::unordered_map<std::string, std::string>* usages,
+            const char* indent = "") EXCLUDES(mMutex);
+
+private:
+    // Mutex to protect records
+    mutable Mutex mMutex;
+
+    // Looper to message the collection thread
+    android::sp<LooperWrapper> mLooper;
+
+    // Background thread to pull stats from the clients
+    std::thread mCollectionThread;
+
+    // Current state of the monitor
+    CollectionEvent mCurrentCollectionEvent GUARDED_BY(mMutex);
+
+    // Periodic collection information
+    CollectionInfo  mPeriodicCollectionInfo GUARDED_BY(mMutex);
+
+    // A collection during the custom period the user sets
+    CollectionInfo  mCustomCollectionInfo GUARDED_BY(mMutex);
+
+    // A list of HalCamera objects to monitor
+    std::unordered_map<std::string,
+                       android::wp<HalCamera>> mClientsToMonitor GUARDED_BY(mMutex);
+
+    // Handles the messages from the looper
+    void handleMessage(const Message& message) override;
+
+    // Handles each CollectionEvent
+    android::base::Result<void> handleCollectionEvent(
+            CollectionEvent event, CollectionInfo* info) EXCLUDES(mMutex);
+
+    // Pulls the statistics from each active HalCamera objects and generates the
+    // records
+    android::base::Result<void> collectLocked(CollectionInfo* info) REQUIRES(mMutex);
+
+    // Returns a string corresponding to a given collection event
+    std::string toString(const CollectionEvent& event) const;
+};
+
+} // namespace implementation
+} // namespace V1_1
+} // namespace evs
+} // namespace automotive
+} // namespace android
+
+#endif // ANDROID_AUTOMOTIVE_EVS_V1_1_EVSSTATSCOLLECTOR_H
diff --git a/evs/sampleDriver/Android.bp b/evs/sampleDriver/Android.bp
index 1722200..1bee713 100644
--- a/evs/sampleDriver/Android.bp
+++ b/evs/sampleDriver/Android.bp
@@ -58,10 +58,6 @@
 
     init_rc: ["android.hardware.automotive.evs@1.1-sample.rc"],
 
-    strip: {
-        keep_symbols: true,
-    },
-
     cflags: ["-DLOG_TAG=\"EvsSampleDriver\""] + [
         "-DGL_GLEXT_PROTOTYPES",
         "-DEGL_EGLEXT_PROTOTYPES",
diff --git a/evs/sampleDriver/EvsEnumerator.cpp b/evs/sampleDriver/EvsEnumerator.cpp
index e108007..ef6009e 100644
--- a/evs/sampleDriver/EvsEnumerator.cpp
+++ b/evs/sampleDriver/EvsEnumerator.cpp
@@ -534,6 +534,12 @@
     }
 
     // Create a new display interface and return it
+    if (sDisplayPortList.find(port) == sDisplayPortList.end()) {
+        LOG(ERROR) << "No display is available on the port "
+                   << static_cast<int32_t>(port);
+        return nullptr;
+    }
+
     pActiveDisplay = new EvsGlDisplay(sDisplayProxy, sDisplayPortList[port]);
     sActiveDisplay = pActiveDisplay;
 
diff --git a/evs/sepolicy/evs_manager.te b/evs/sepolicy/evs_manager.te
index d3ef6db..cf649bb 100644
--- a/evs/sepolicy/evs_manager.te
+++ b/evs/sepolicy/evs_manager.te
@@ -13,3 +13,6 @@
 # allow write to fd
 allow evs_manager shell:fd use;
 allow evs_manager shell:fifo_file write;
+
+# allow evs_manager to send information to statsd socket
+unix_socket_send(evs_manager, statsdw, statsd)
diff --git a/evs/support_library/Android.bp b/evs/support_library/Android.bp
index 6556404..8b77a5d 100644
--- a/evs/support_library/Android.bp
+++ b/evs/support_library/Android.bp
@@ -56,10 +56,6 @@
         "camera_config.json",
     ],
 
-    strip: {
-        keep_symbols: true,
-    },
-
     cflags: ["-DLOG_TAG=\"libevssupport\""] + [
         "-DGL_GLEXT_PROTOTYPES",
         "-DEGL_EGLEXT_PROTOTYPES",
diff --git a/service/res/values-ne/strings.xml b/service/res/values-ne/strings.xml
index a03753b..df9f269 100644
--- a/service/res/values-ne/strings.xml
+++ b/service/res/values-ne/strings.xml
@@ -46,7 +46,7 @@
     <string name="car_permission_label_audio_settings" msgid="6524703796944023977">"कारका अडियो सेटिङ व्यवस्थित गर्ने"</string>
     <string name="car_permission_label_mock_vehicle_hal" msgid="7198852512207405935">"सवारी साधनको HAL को अनुकरण गर्ने"</string>
     <string name="car_permission_label_receive_ducking" msgid="4884538660766756573">"अडियो डकिङ कार्यक्रमहरू प्राप्त गर्नुहोस्"</string>
-    <string name="car_permission_desc_receive_ducking" msgid="776376388266656512">"कारमा अन्य अडियो प्ले भइरहेका हुनाले अनुप्रयोगको भोल्युम कम भइरहेको कुराबारे एपलाई सूचित हुन दिन्छ।"</string>
+    <string name="car_permission_desc_receive_ducking" msgid="776376388266656512">"कारमा अन्य अडियो प्ले भइरहेका हुनाले एपको भोल्युम कम भइरहेको कुराबारे एपलाई सूचित हुन दिन्छ।"</string>
     <string name="car_permission_desc_mock_vehicle_hal" msgid="5235596491098649155">"आन्तरिक परीक्षण गर्ने प्रयोजनका लागि तपाईंको कारको सवारी साधन HAL को अनुकरण गर्ने।"</string>
     <string name="car_permission_desc_audio_volume" msgid="536626185654307889">"तपाईंको कारको अडियोको भोल्युम नियन्त्रण गर्ने।"</string>
     <string name="car_permission_desc_audio_settings" msgid="7192007170677915937">"आफ्नो कारको अडियोसम्बन्धी सेटिङहरू नियन्त्रण गर्नुहोस्।"</string>
diff --git a/service/src/com/android/car/CarBugreportManagerService.java b/service/src/com/android/car/CarBugreportManagerService.java
index 2480657..39f4461 100644
--- a/service/src/com/android/car/CarBugreportManagerService.java
+++ b/service/src/com/android/car/CarBugreportManagerService.java
@@ -30,6 +30,7 @@
 import android.net.LocalSocket;
 import android.net.LocalSocketAddress;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.ParcelFileDescriptor;
@@ -50,6 +51,7 @@
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Bugreport service for cars.
@@ -69,7 +71,11 @@
     private static final String OK_PREFIX = "OK:";
     private static final String FAIL_PREFIX = "FAIL:";
 
+    /**
+     * The services are defined in {@code packages/services/Car/car-bugreportd/car-bugreportd.rc}.
+     */
     private static final String BUGREPORTD_SERVICE = "car-bugreportd";
+    private static final String DUMPSTATEZ_SERVICE = "car-dumpstatez";
 
     // The socket definitions must match the actual socket names defined in car_bugreportd service
     // definition.
@@ -86,7 +92,7 @@
     private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
             getClass().getSimpleName());
     private final Handler mHandler = new Handler(mHandlerThread.getLooper());
-    private boolean mIsServiceRunning;
+    private final AtomicBoolean mIsServiceRunning = new AtomicBoolean(false);
 
     /**
      * Create a CarBugreportManagerService instance.
@@ -111,65 +117,104 @@
     @RequiresPermission(android.Manifest.permission.DUMP)
     public void requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
             ICarBugreportCallback callback) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.DUMP, "requestBugreport");
+        ensureTheCallerIsSignedWithPlatformKeys();
+        ensureTheCallerIsDesignatedBugReportApp();
+        synchronized (mLock) {
+            if (mIsServiceRunning.getAndSet(true)) {
+                Slog.w(TAG, "Bugreport Service already running");
+                reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
+                return;
+            }
+            requestBugReportLocked(output, extraOutput, callback);
+        }
+    }
 
-        // Check the caller has proper permission
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
-                "requestZippedBugreport");
-        // Check the caller is signed with platform keys
+    @Override
+    @RequiresPermission(android.Manifest.permission.DUMP)
+    public void cancelBugreport() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.DUMP, "cancelBugreport");
+        ensureTheCallerIsSignedWithPlatformKeys();
+        ensureTheCallerIsDesignatedBugReportApp();
+        synchronized (mLock) {
+            if (!mIsServiceRunning.getAndSet(false)) {
+                Slog.i(TAG, "Failed to cancel. Service is not running.");
+                return;
+            }
+            Slog.i(TAG, "Cancelling the running bugreport");
+            mHandler.removeCallbacksAndMessages(/* token= */ null);
+            // This tells init to cancel the services. Note that this is achieved through
+            // setting a system property which is not thread-safe. So the lock here offers
+            // thread-safety only among callers of the API.
+            try {
+                SystemProperties.set("ctl.stop", BUGREPORTD_SERVICE);
+            } catch (RuntimeException e) {
+                Slog.e(TAG, "Failed to stop " + BUGREPORTD_SERVICE, e);
+            }
+            try {
+                // Stop DUMPSTATEZ_SERVICE service too, because stopping BUGREPORTD_SERVICE doesn't
+                // guarantee stopping DUMPSTATEZ_SERVICE.
+                SystemProperties.set("ctl.stop", DUMPSTATEZ_SERVICE);
+            } catch (RuntimeException e) {
+                Slog.e(TAG, "Failed to stop " + DUMPSTATEZ_SERVICE, e);
+            }
+        }
+    }
+
+    private void ensureTheCallerIsSignedWithPlatformKeys() {
         PackageManager pm = mContext.getPackageManager();
         int callingUid = Binder.getCallingUid();
         if (pm.checkSignatures(Process.myUid(), callingUid) != PackageManager.SIGNATURE_MATCH) {
             throw new SecurityException("Caller " + pm.getNameForUid(callingUid)
                             + " does not have the right signature");
         }
-        // Check if the caller is the designated bugreport app
+    }
+
+    /** Checks only on user builds. */
+    private void ensureTheCallerIsDesignatedBugReportApp() {
+        if (Build.IS_DEBUGGABLE) {
+            // Per https://source.android.com/setup/develop/new-device, user builds are debuggable=0
+            return;
+        }
         String defaultAppPkgName = mContext.getString(R.string.config_car_bugreport_application);
+        int callingUid = Binder.getCallingUid();
+        PackageManager pm = mContext.getPackageManager();
         String[] packageNamesForCallerUid = pm.getPackagesForUid(callingUid);
-        boolean found = false;
         if (packageNamesForCallerUid != null) {
             for (String packageName : packageNamesForCallerUid) {
                 if (defaultAppPkgName.equals(packageName)) {
-                    found = true;
-                    break;
+                    return;
                 }
             }
         }
-        if (!found) {
-            throw new SecurityException("Caller " +  pm.getNameForUid(callingUid)
-                    + " is not a designated bugreport app");
-        }
-
-        synchronized (mLock) {
-            requestBugReportLocked(output, extraOutput, callback);
-        }
+        throw new SecurityException("Caller " +  pm.getNameForUid(callingUid)
+                + " is not a designated bugreport app");
     }
 
     @GuardedBy("mLock")
     private void requestBugReportLocked(ParcelFileDescriptor output,
             ParcelFileDescriptor extraOutput, ICarBugreportCallback callback) {
-        if (mIsServiceRunning) {
-            Slog.w(TAG, "Bugreport Service already running");
-            reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
-            return;
-        }
-        mIsServiceRunning = true;
-        mHandler.post(() -> startBugreportd(output, extraOutput, callback));
-    }
-
-    private void startBugreportd(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
-            ICarBugreportCallback callback) {
         Slog.i(TAG, "Starting " + BUGREPORTD_SERVICE);
         try {
+            // This tells init to start the service. Note that this is achieved through
+            // setting a system property which is not thread-safe. So the lock here offers
+            // thread-safety only among callers of the API.
             SystemProperties.set("ctl.start", BUGREPORTD_SERVICE);
         } catch (RuntimeException e) {
+            mIsServiceRunning.set(false);
             Slog.e(TAG, "Failed to start " + BUGREPORTD_SERVICE, e);
             reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
             return;
         }
-        processBugreportSockets(output, extraOutput, callback);
-        synchronized (mLock) {
-            mIsServiceRunning = false;
-        }
+        mHandler.post(() -> {
+            try {
+                processBugreportSockets(output, extraOutput, callback);
+            } finally {
+                mIsServiceRunning.set(false);
+            }
+        });
     }
 
     private void handleProgress(String line, ICarBugreportCallback callback) {
@@ -231,11 +276,10 @@
             reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
             return;
         }
-
         try (BufferedReader reader =
                 new BufferedReader(new InputStreamReader(localSocket.getInputStream()))) {
             String line;
-            while ((line = reader.readLine()) != null) {
+            while (mIsServiceRunning.get() && (line = reader.readLine()) != null) {
                 if (line.startsWith(PROGRESS_PREFIX)) {
                     handleProgress(line, callback);
                 } else if (line.startsWith(FAIL_PREFIX)) {
@@ -284,7 +328,7 @@
         try {
             callback.onError(errorCode);
         } catch (RemoteException e) {
-            Slog.e(TAG, "onError() failed: " + e.getMessage());
+            Slog.e(TAG, "onError() failed", e);
         }
     }
 
@@ -309,7 +353,17 @@
             // Therefore we are generous in setting the timeout. Most cases should not even
             // come close to the timeouts, but since bugreports are taken when there is a
             // system issue, it is hard to guess.
-            SystemClock.sleep(SOCKET_CONNECTION_RETRY_DELAY_IN_MS);
+            // The following lines waits for SOCKET_CONNECTION_RETRY_DELAY_IN_MS or until
+            // mIsServiceRunning becomes false.
+            for (int i = 0; i < SOCKET_CONNECTION_RETRY_DELAY_IN_MS / 50; i++) {
+                if (!mIsServiceRunning.get()) {
+                    Slog.i(TAG, "Failed to connect to socket " + socketName
+                            + ". The service is prematurely cancelled.");
+                    return null;
+                }
+                SystemClock.sleep(50);  // Millis.
+            }
+
             try {
                 socket.connect(new LocalSocketAddress(socketName,
                         LocalSocketAddress.Namespace.RESERVED));
@@ -320,7 +374,7 @@
                             + " after " + retryCount + " retries", e);
                     return null;
                 }
-                Log.i(TAG, "Failed to connect to " + socketName + ". Will try again "
+                Log.i(TAG, "Failed to connect to " + socketName + ". Will try again. "
                         + e.getMessage());
             }
         }
diff --git a/service/src/com/android/car/CarPowerManagementService.java b/service/src/com/android/car/CarPowerManagementService.java
index d130c43..aa84b59 100644
--- a/service/src/com/android/car/CarPowerManagementService.java
+++ b/service/src/com/android/car/CarPowerManagementService.java
@@ -16,6 +16,7 @@
 package com.android.car;
 
 import android.annotation.NonNull;
+import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.car.Car;
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
@@ -23,6 +24,7 @@
 import android.car.hardware.power.ICarPowerStateListener;
 import android.car.userlib.HalCallback;
 import android.car.userlib.InitialUserSetter;
+import android.car.userlib.InitialUserSetter.InitialUserInfoType;
 import android.car.userlib.UserHalHelper;
 import android.car.userlib.UserHelper;
 import android.content.ComponentName;
@@ -164,14 +166,13 @@
         this(context, context.getResources(), powerHal, systemInterface, UserManager.get(context),
                 carUserService, new InitialUserSetter(context,
                         (u) -> carUserService.setInitialUser(u),
-                        context.getString(R.string.default_guest_name),
-                        !carUserService.isUserHalSupported()));
+                        context.getString(R.string.default_guest_name)));
     }
 
     @VisibleForTesting
-    CarPowerManagementService(Context context, Resources resources, PowerHalService powerHal,
-            SystemInterface systemInterface, UserManager userManager,
-            CarUserService carUserService, InitialUserSetter initialUserSetter) {
+    public CarPowerManagementService(Context context, Resources resources, PowerHalService powerHal,
+            SystemInterface systemInterface, UserManager userManager, CarUserService carUserService,
+            InitialUserSetter initialUserSetter) {
         mContext = context;
         mHal = powerHal;
         mSystemInterface = systemInterface;
@@ -194,7 +195,7 @@
     }
 
     @VisibleForTesting
-    protected void setShutdownTimersForTest(int pollingIntervalMs, int shutdownTimeoutMs) {
+    public void setShutdownTimersForTest(int pollingIntervalMs, int shutdownTimeoutMs) {
         // Override timers to keep testing time short
         // Passing in '0' resets the value to the default
         synchronized (mLock) {
@@ -431,7 +432,13 @@
             return;
         }
 
-        mInitialUserSetter.executeDefaultBehavior(/* replaceGuest= */ !mSwitchGuestUserBeforeSleep);
+        executeDefaultInitialUserBehavior(!mSwitchGuestUserBeforeSleep);
+    }
+
+    private void executeDefaultInitialUserBehavior(boolean replaceGuest) {
+        mInitialUserSetter.set(newInitialUserInfoBuilder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+                .setReplaceGuest(replaceGuest)
+                .build());
     }
 
     /**
@@ -447,10 +454,20 @@
         boolean replaceGuest = !mSwitchGuestUserBeforeSleep;
         if (newUser == null) {
             Log.w(TAG, "Failed to replace guest; falling back to default behavior");
-            mInitialUserSetter.executeDefaultBehavior(replaceGuest);
+            executeDefaultInitialUserBehavior(replaceGuest);
             return;
         }
-        mInitialUserSetter.switchUser(newUser.id, replaceGuest);
+        switchUser(newUser.id, replaceGuest);
+    }
+
+    private void switchUser(@UserIdInt int userId, boolean replaceGuest) {
+        mInitialUserSetter.set(newInitialUserInfoBuilder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(userId).setReplaceGuest(replaceGuest).build());
+    }
+
+    private InitialUserSetter.Builder newInitialUserInfoBuilder(@InitialUserInfoType int type) {
+        return new InitialUserSetter.Builder(type)
+                .setSupportsOverrideUserIdProperty(!mUserService.isUserHalSupported());
     }
 
     /**
@@ -479,14 +496,14 @@
                     switch (response.action) {
                         case InitialUserInfoResponseAction.DEFAULT:
                             Log.i(TAG, "HAL requested default initial user behavior");
-                            mInitialUserSetter.executeDefaultBehavior(replaceGuest);
+                            executeDefaultInitialUserBehavior(replaceGuest);
                             return;
                         case InitialUserInfoResponseAction.SWITCH:
                             int userId = response.userToSwitchOrCreate.userId;
                             Log.i(TAG, "HAL requested switch to user " + userId);
                             // If guest was replaced on shutdown, it doesn't need to be replaced
                             // again
-                            mInitialUserSetter.switchUser(userId, replaceGuest);
+                            switchUser(userId, replaceGuest);
                             return;
                         case InitialUserInfoResponseAction.CREATE:
                             int halFlags = response.userToSwitchOrCreate.flags;
@@ -494,7 +511,11 @@
                             Log.i(TAG, "HAL requested new user (name="
                                     + UserHelper.safeName(name) + ", flags="
                                     + UserHalHelper.userFlagsToString(halFlags) + ")");
-                            mInitialUserSetter.createUser(name, halFlags);
+                            mInitialUserSetter
+                                    .set(newInitialUserInfoBuilder(InitialUserSetter.TYPE_CREATE)
+                                            .setNewUserName(name)
+                                            .setNewUserFlags(halFlags)
+                                            .build());
                             return;
                         default:
                             switchUserOnResumeUserHalFallback(
@@ -513,7 +534,7 @@
     private void switchUserOnResumeUserHalFallback(String reason) {
         Log.w(TAG, "Failed to set initial user based on User Hal (" + reason
                 + "); falling back to default behavior");
-        mInitialUserSetter.executeDefaultBehavior(/* replaceGuest= */ !mSwitchGuestUserBeforeSleep);
+        executeDefaultInitialUserBehavior(!mSwitchGuestUserBeforeSleep);
     }
 
     private void handleShutdownPrepare(CpmsState newState) {
diff --git a/service/src/com/android/car/CarPropertyService.java b/service/src/com/android/car/CarPropertyService.java
index 39a01e7..9f7bd2b 100644
--- a/service/src/com/android/car/CarPropertyService.java
+++ b/service/src/com/android/car/CarPropertyService.java
@@ -25,6 +25,8 @@
 import android.car.hardware.property.ICarProperty;
 import android.car.hardware.property.ICarPropertyEventListener;
 import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
@@ -61,6 +63,10 @@
     private final Object mLock = new Object();
     @GuardedBy("mLock")
     private final SparseArray<SparseArray<Client>> mSetOperationClientMap = new SparseArray<>();
+    private final HandlerThread mHandlerThread =
+            CarServiceUtils.getHandlerThread(getClass().getSimpleName());
+    private final Handler mHandler = new Handler(mHandlerThread.getLooper());
+
     public CarPropertyService(Context context, PropertyHalService hal) {
         if (DBG) {
             Log.d(TAG, "CarPropertyService started!");
@@ -169,21 +175,24 @@
         if (DBG) {
             Log.d(TAG, "registerListener: propId=0x" + toHexString(propId) + " rate=" + rate);
         }
-        if (mConfigs.get(propId) == null) {
-            // Do not attempt to register an invalid propId
-            Log.e(TAG, "registerListener:  propId is not in config list: 0x" + toHexString(propId));
-            return;
-        }
-        ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId));
         if (listener == null) {
             Log.e(TAG, "registerListener: Listener is null.");
             throw new IllegalArgumentException("listener cannot be null.");
         }
 
         IBinder listenerBinder = listener.asBinder();
-
+        CarPropertyConfig propertyConfig;
+        Client finalClient;
         synchronized (mLock) {
-            // Get the client for this listener
+            propertyConfig = mConfigs.get(propId);
+            if (propertyConfig == null) {
+                // Do not attempt to register an invalid propId
+                Log.e(TAG, "registerListener:  propId is not in config list: 0x" + toHexString(
+                        propId));
+                return;
+            }
+            ICarImpl.assertPermission(mContext, mHal.getReadPermission(propId));
+            // Get or create the client for this listener
             Client client = mClientMap.get(listenerBinder);
             if (client == null) {
                 client = new Client(listener);
@@ -206,29 +215,36 @@
             if (rate > mHal.getSampleRate(propId)) {
                 mHal.subscribeProperty(propId, rate);
             }
+            finalClient = client;
         }
-        // Send the latest value(s) to the registering listener only
-        List<CarPropertyEvent> events = new LinkedList<CarPropertyEvent>();
-        if (mConfigs.get(propId).isGlobalProperty()) {
+
+        // propertyConfig and client are NonNull.
+        mHandler.post(() ->
+                getAndDispatchPropertyInitValue(propertyConfig, finalClient));
+    }
+
+    private void getAndDispatchPropertyInitValue(CarPropertyConfig config, Client client) {
+        List<CarPropertyEvent> events = new LinkedList<>();
+        int propId = config.getPropertyId();
+        if (config.isGlobalProperty()) {
             CarPropertyValue value = mHal.getProperty(propId, 0);
-            // CarPropertyEvent without a CarPropertyValue can not be used by any listeners.
             if (value != null) {
                 CarPropertyEvent event = new CarPropertyEvent(
-                    CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
+                        CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
                 events.add(event);
             }
         } else {
-            for (int areaId : mConfigs.get(propId).getAreaIds()) {
+            for (int areaId : config.getAreaIds()) {
                 CarPropertyValue value = mHal.getProperty(propId, areaId);
                 if (value != null) {
                     CarPropertyEvent event = new CarPropertyEvent(
-                        CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
+                            CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE, value);
                     events.add(event);
                 }
             }
         }
         try {
-            listener.onEvent(events);
+            client.getListener().onEvent(events);
         } catch (RemoteException ex) {
             // If we cannot send a record, its likely the connection snapped. Let the binder
             // death handle the situation.
diff --git a/service/src/com/android/car/CarShellCommand.java b/service/src/com/android/car/CarShellCommand.java
index 871e18c..3a91aec 100644
--- a/service/src/com/android/car/CarShellCommand.java
+++ b/service/src/com/android/car/CarShellCommand.java
@@ -26,6 +26,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.UiModeManager;
@@ -33,16 +34,18 @@
 import android.car.input.CarInputManager;
 import android.car.input.RotaryEvent;
 import android.car.user.CarUserManager;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
 import android.car.user.UserSwitchResult;
 import android.car.userlib.HalCallback;
 import android.car.userlib.UserHalHelper;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponse;
 import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
 import android.hardware.automotive.vehicle.V2_0.SwitchUserMessageType;
 import android.hardware.automotive.vehicle.V2_0.SwitchUserStatus;
+import android.hardware.automotive.vehicle.V2_0.UserFlags;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociation;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationType;
@@ -286,8 +289,11 @@
         pw.println("\t  Print this help text.");
         pw.println("\tday-night-mode [day|night|sensor]");
         pw.println("\t  Force into day/night mode or restore to auto.");
-        pw.println("\tinject-vhal-event property [zone] data(can be comma separated list)");
+        pw.println("\tinject-vhal-event property [zone] data(can be comma separated list) "
+                + "[-t delay_time_seconds]");
         pw.println("\t  Inject a vehicle property for testing.");
+        pw.println("\t  delay_time_seconds: the event timestamp is increased by certain second.");
+        pw.println("\t  If not specified, it will be 0.");
         pw.println("\tinject-error-event property zone errorCode");
         pw.println("\t  Inject an error event from VHAL for testing.");
         pw.println("\tenable-uxr true|false");
@@ -345,12 +351,14 @@
         pw.println("\t  delta_times_ms: a list of delta time (current time minus event time)");
         pw.println("\t                  in descending order. If not specified, it will be 0.");
 
-        pw.printf("\t%s <REQ_TYPE> [--timeout TIMEOUT_MS]\n", COMMAND_GET_INITIAL_USER_INFO);
+        pw.printf("\t%s <REQ_TYPE> [--hal-only] [--timeout TIMEOUT_MS]\n",
+                COMMAND_GET_INITIAL_USER_INFO);
         pw.println("\t  Calls the Vehicle HAL to get the initial boot info, passing the given");
         pw.println("\t  REQ_TYPE (which could be either FIRST_BOOT, FIRST_BOOT_AFTER_OTA, ");
         pw.println("\t  COLD_BOOT, RESUME, or any numeric value that would be passed 'as-is')");
         pw.println("\t  and an optional TIMEOUT_MS to wait for the HAL response (if not set,");
         pw.println("\t  it will use a  default value).");
+        pw.println("\t  The --hal-only option only calls HAL, without using CarUserService.");
 
         pw.printf("\t%s <USER_ID> [--hal-only] [--timeout TIMEOUT_MS]\n", COMMAND_SWITCH_USER);
         pw.println("\t  Switches to user USER_ID using the HAL integration.");
@@ -439,9 +447,12 @@
             case COMMAND_INJECT_VHAL_EVENT:
                 String zone = PARAM_VEHICLE_PROPERTY_AREA_GLOBAL;
                 String data;
-                if (args.length != 3 && args.length != 4) {
+                int argNum = args.length;
+                if (argNum < 3 || argNum > 6) {
                     return showInvalidArguments(writer);
-                } else if (args.length == 4) {
+                }
+                String delayTime = args[argNum - 2].equals("-t") ?  args[argNum - 1] : "0";
+                if (argNum == 4 || argNum == 6) {
                     // Zoned
                     zone = args[2];
                     data = args[3];
@@ -449,7 +460,7 @@
                     // Global
                     data = args[2];
                 }
-                injectVhalEvent(args[1], zone, data, false, writer);
+                injectVhalEvent(args[1], zone, data, false, delayTime, writer);
                 break;
             case COMMAND_INJECT_ERROR_EVENT:
                 if (args.length != 4) {
@@ -457,7 +468,7 @@
                 }
                 String errorAreaId = args[2];
                 String errorCode = args[3];
-                injectVhalEvent(args[1], errorAreaId, errorCode, true, writer);
+                injectVhalEvent(args[1], errorAreaId, errorCode, true, "0", writer);
                 break;
             case COMMAND_ENABLE_UXR:
                 if (args.length != 2) {
@@ -814,6 +825,7 @@
         // Gets the request type
         String typeArg = args[1];
         int requestType = UserHalHelper.parseInitialUserInfoRequestType(typeArg);
+        boolean halOnly = false;
 
         int timeout = DEFAULT_HAL_TIMEOUT_MS;
         for (int i = 2; i < args.length; i++) {
@@ -822,6 +834,9 @@
                 case "--timeout":
                     timeout = Integer.parseInt(args[++i]);
                     break;
+                case "--hal-only":
+                    halOnly = true;
+                    break;
                 default:
                     writer.println("Invalid option at index " + i + ": " + arg);
                     return;
@@ -832,12 +847,8 @@
         Log.d(TAG, "handleGetInitialUserInfo(): type=" + requestType + " (" + typeArg
                 + "), timeout=" + timeout);
 
-        UserHalService userHal = mHal.getUserHal();
-        // TODO(b/150413515): use UserHalHelper to populate it with current users
-        UsersInfo usersInfo = new UsersInfo();
         CountDownLatch latch = new CountDownLatch(1);
-
-        userHal.getInitialUserInfo(requestType, timeout, usersInfo, (status, resp) -> {
+        HalCallback<InitialUserInfoResponse> callback = (status, resp) -> {
             try {
                 Log.d(TAG, "GetUserInfoResponse: status=" + status + ", resp=" + resp);
                 writer.printf("Call status: %s\n",
@@ -848,10 +859,30 @@
                 writer.printf("Request id: %d\n", resp.requestId);
                 writer.printf("Action: %s\n",
                         InitialUserInfoResponseAction.toString(resp.action));
+                if (!TextUtils.isEmpty(resp.userNameToCreate)) {
+                    writer.printf("User name: %s\n", resp.userNameToCreate);
+                }
+                if (resp.userToSwitchOrCreate.userId != UserHandle.USER_NULL) {
+                    writer.printf("User id: %d\n", resp.userToSwitchOrCreate.userId);
+                }
+                if (resp.userToSwitchOrCreate.flags != UserFlags.NONE) {
+                    writer.printf("User flags: %s\n",
+                            UserHalHelper.userFlagsToString(resp.userToSwitchOrCreate.flags));
+                }
+                if (!TextUtils.isEmpty(resp.userLocales)) {
+                    writer.printf("User locales: %s\n", resp.userLocales);
+                }
             } finally {
                 latch.countDown();
             }
-        });
+        };
+        if (halOnly) {
+            // TODO(b/150413515): use UserHalHelper to populate it with current users
+            UsersInfo usersInfo = new UsersInfo();
+            mHal.getUserHal().getInitialUserInfo(requestType, timeout, usersInfo, callback);
+        } else {
+            mCarUserService.getInitialUserInfo(requestType, callback);
+        }
         waitForHal(writer, latch, timeout);
     }
 
@@ -929,20 +960,10 @@
             waitForHal(writer, latch, timeout);
             return;
         }
-        Car car = Car.createCar(mContext);
-        CarUserManager carUserManager =
-                (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
+        CarUserManager carUserManager = getCarUserManager(mContext);
         AndroidFuture<UserSwitchResult> future = carUserManager.switchUser(targetUserId);
-        UserSwitchResult result = null;
-        try {
-            result = future.get(timeout, TimeUnit.MILLISECONDS);
-        } catch (Exception e) {
-            Log.e(TAG, "exception calling CarUserManager.switchUser(" + targetUserId + ")", e);
-        }
-        if (result == null) {
-            writer.printf("Service didn't respond in %d ms", timeout);
-            return;
-        }
+        UserSwitchResult result = waitForFuture(writer, future, timeout);
+        if (result == null) return;
         writer.printf("UserSwitchResult: status = %s\n",
                 UserSwitchResult.statusToString(result.getStatus()));
         String msg = result.getErrorMessage();
@@ -951,6 +972,20 @@
         }
     }
 
+    private static <T> T waitForFuture(@NonNull PrintWriter writer,
+            @NonNull AndroidFuture<T> future, int timeoutMs) {
+        T result = null;
+        try {
+            result = future.get(timeoutMs, TimeUnit.MILLISECONDS);
+            if (result == null) {
+                writer.printf("Service didn't respond in %d ms", timeoutMs);
+            }
+        } catch (Exception e) {
+            writer.printf("Exception getting future: %s",  e);
+        }
+        return result;
+    }
+
     private void getInitialUser(PrintWriter writer) {
         android.content.pm.UserInfo user = mCarUserService.getInitialUser();
         writer.println(user == null ? NO_INITIAL_USER : user.id);
@@ -1004,24 +1039,21 @@
                     + ", request=" + request);
             UserIdentificationResponse response = mHal.getUserHal().getUserAssociation(request);
             Log.d(TAG, "getUserAuthAssociation(): response=" + response);
-
-            if (response == null) {
-                writer.println("null response");
-                return;
-            }
-
-            if (!TextUtils.isEmpty(response.errorMessage)) {
-                writer.printf("Error message: %s\n", response.errorMessage);
-            }
-            int numberAssociations = response.associations.size();
-            writer.printf("%d associations:\n", numberAssociations);
-            for (int i = 0; i < numberAssociations; i++) {
-                UserIdentificationAssociation association = response.associations.get(i);
-                writer.printf("  %s\n", association);
-            }
+            showResponse(writer, response);
             return;
         }
 
+        CarUserManager carUserManager = getCarUserManager(writer, userId);
+        int[] types = new int[requestSize];
+        for (int i = 0; i < requestSize; i++) {
+            types[i] = request.associationTypes.get(i);
+        }
+        UserIdentificationAssociationResponse response = carUserManager
+                .getUserIdentificationAssociation(types);
+        showResponse(writer, response);
+    }
+
+    private CarUserManager getCarUserManager(@NonNull PrintWriter writer, @UserIdInt int userId) {
         Context context;
         if (userId == mContext.getUserId()) {
             context = mContext;
@@ -1034,14 +1066,35 @@
                     + "what CarUserService will use when calling HAL.\n", userId, actualUserId);
         }
 
+        return getCarUserManager(context);
+    }
+
+    private CarUserManager getCarUserManager(@NonNull Context context) {
         Car car = Car.createCar(context);
         CarUserManager carUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
-        int[] types = new int[requestSize];
-        for (int i = 0; i < types.length; i++) {
-            types[i] = request.associationTypes.get(i);
+        return carUserManager;
+    }
+
+    private void showResponse(@NonNull PrintWriter writer,
+            @NonNull UserIdentificationResponse response) {
+        if (response == null) {
+            writer.println("null response");
+            return;
         }
-        GetUserIdentificationAssociationResponse response = carUserManager
-                .getUserIdentificationAssociation(types);
+
+        if (!TextUtils.isEmpty(response.errorMessage)) {
+            writer.printf("Error message: %s\n", response.errorMessage);
+        }
+        int numberAssociations = response.associations.size();
+        writer.printf("%d associations:\n", numberAssociations);
+        for (int i = 0; i < numberAssociations; i++) {
+            UserIdentificationAssociation association = response.associations.get(i);
+            writer.printf("  %s\n", association);
+        }
+    }
+
+    private void showResponse(@NonNull PrintWriter writer,
+            @NonNull UserIdentificationAssociationResponse response) {
         if (response == null) {
             writer.println("null response");
             return;
@@ -1058,7 +1111,7 @@
     }
 
     private void setUserAuthAssociation(String[] args, PrintWriter writer) {
-        if (args.length < 4) {
+        if (args.length < 3) {
             writer.println("invalid usage, must pass at least 4 arguments");
             return;
         }
@@ -1119,20 +1172,7 @@
             mHal.getUserHal().setUserAssociation(timeout, request, (status, response) -> {
                 Log.d(TAG, "setUserAuthAssociation(): response=" + response);
                 try {
-                    if (response == null) {
-                        writer.println("null response");
-                        return;
-                    }
-
-                    if (!TextUtils.isEmpty(response.errorMessage)) {
-                        writer.printf("Error message: %s\n", response.errorMessage);
-                    }
-                    int numberAssociations = response.associations.size();
-                    writer.printf("%d associations:\n", numberAssociations);
-                    for (int i = 0; i < numberAssociations; i++) {
-                        UserIdentificationAssociation association = response.associations.get(i);
-                        writer.printf("  %s\n", association);
-                    }
+                    showResponse(writer, response);
                 } finally {
                     latch.countDown();
                 }
@@ -1140,8 +1180,20 @@
             waitForHal(writer, latch, timeout);
             return;
         }
-        // TODO(b/150409351): implement it...
-        throw new UnsupportedOperationException("must set --hal-only");
+        CarUserManager carUserManager = getCarUserManager(writer, userId);
+        int[] types = new int[requestSize];
+        int[] values = new int[requestSize];
+        for (int i = 0; i < requestSize; i++) {
+            UserIdentificationSetAssociation association = request.associations.get(i);
+            types[i] = association.type;
+            values[i] = association.value;
+        }
+        AndroidFuture<UserIdentificationAssociationResponse> future = carUserManager
+                .setUserIdentificationAssociation(types, values);
+        UserIdentificationAssociationResponse response = waitForFuture(writer, future, timeout);
+        if (response != null) {
+            showResponse(writer, response);
+        }
     }
 
     private static int parseAuthArg(@NonNull SparseArray<String> types, @NonNull String type) {
@@ -1218,10 +1270,11 @@
      * @param zone     Zone that this event services
      * @param isErrorEvent indicates the type of event
      * @param value    Data value of the event
+     * @param delayTime the event timestamp is increased by delayTime
      * @param writer   PrintWriter
      */
     private void injectVhalEvent(String property, String zone, String value,
-            boolean isErrorEvent, PrintWriter writer) {
+            boolean isErrorEvent, String delayTime, PrintWriter writer) {
         if (zone != null && (zone.equalsIgnoreCase(PARAM_VEHICLE_PROPERTY_AREA_GLOBAL))) {
             if (!isPropertyAreaTypeGlobal(property)) {
                 writer.println("Property area type inconsistent with given zone");
@@ -1232,7 +1285,7 @@
             if (isErrorEvent) {
                 mHal.injectOnPropertySetError(property, zone, value);
             } else {
-                mHal.injectVhalEvent(property, zone, value);
+                mHal.injectVhalEvent(property, zone, value, delayTime);
             }
         } catch (NumberFormatException e) {
             writer.println("Invalid property Id zone Id or value" + e);
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 972f0a7..53e76fa 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -268,6 +268,7 @@
         CarLocalServices.addService(FixedActivityService.class, mFixedActivityService);
         CarLocalServices.addService(VmsBrokerService.class, mVmsBrokerService);
         CarLocalServices.addService(CarOccupantZoneService.class, mCarOccupantZoneService);
+        CarLocalServices.addService(AppFocusService.class, mAppFocusService);
 
         // Be careful with order. Service depending on other service should be inited later.
         List<CarServiceBase> allServices = new ArrayList<>();
diff --git a/service/src/com/android/car/VmsPublishersInfo.java b/service/src/com/android/car/VmsPublishersInfo.java
deleted file mode 100644
index b99fc1e..0000000
--- a/service/src/com/android/car/VmsPublishersInfo.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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 android.annotation.Nullable;
-import android.util.ArrayMap;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-
-public class VmsPublishersInfo {
-    private static final byte[] EMPTY_RESPONSE = new byte[0];
-
-    private final Object mLock = new Object();
-    @GuardedBy("mLock")
-    private final ArrayMap<InfoWrapper, Integer> mPublishersIds = new ArrayMap<>();
-    @GuardedBy("mLock")
-    private final ArrayList<InfoWrapper> mPublishersInfo = new ArrayList<>();
-
-    private static class InfoWrapper {
-        private final byte[] mInfo;
-
-        InfoWrapper(byte[] info) {
-            mInfo = info;
-        }
-
-        public byte[] getInfo() {
-            return mInfo.clone();
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (!(o instanceof InfoWrapper)) {
-                return false;
-            }
-            InfoWrapper p = (InfoWrapper) o;
-            return Arrays.equals(this.mInfo, p.mInfo);
-        }
-
-        @Override
-        public int hashCode() {
-            return Arrays.hashCode(mInfo);
-        }
-    }
-
-    /**
-     * Retrieves the publisher ID for the given publisher information. If the publisher information
-     * has not previously been seen, it will be assigned a new publisher ID.
-     *
-     * @param publisherInfo Publisher information to query or register.
-     * @return Publisher ID for the given publisher information.
-     */
-    public int getIdForInfo(byte[] publisherInfo) {
-        Integer publisherId;
-        InfoWrapper wrappedPublisherInfo = new InfoWrapper(publisherInfo);
-        synchronized (mLock) {
-            // Check if publisher is already registered
-            publisherId = mPublishersIds.get(wrappedPublisherInfo);
-            if (publisherId == null) {
-                // Add the new publisher and assign it the next ID
-                mPublishersInfo.add(wrappedPublisherInfo);
-                publisherId = mPublishersInfo.size();
-                mPublishersIds.put(wrappedPublisherInfo, publisherId);
-            }
-        }
-        return publisherId;
-    }
-
-    /**
-     * Returns the publisher info associated with the given publisher ID.
-     * @param publisherId Publisher ID to query.
-     * @return Publisher info associated with the ID, or an empty array if publisher ID is unknown.
-     */
-    public byte[] getPublisherInfo(int publisherId) {
-        synchronized (mLock) {
-            byte[] result = getPublisherInfoOrNull(publisherId);
-            return result != null ? result : EMPTY_RESPONSE;
-        }
-    }
-
-    /**
-     * Returns the publisher info associated with the given publisher ID.
-     * @param publisherId Publisher ID to query.
-     * @return Publisher info associated with the ID, or null if publisher ID is unknown.
-     */
-    @Nullable
-    public byte[] getPublisherInfoOrNull(int publisherId) {
-        synchronized (mLock) {
-            return publisherId < 1 || publisherId > mPublishersInfo.size()
-                    ? null
-                    : mPublishersInfo.get(publisherId - 1).getInfo();
-        }
-    }
-}
-
diff --git a/service/src/com/android/car/am/FixedActivityService.java b/service/src/com/android/car/am/FixedActivityService.java
index f381702..cd16463 100644
--- a/service/src/com/android/car/am/FixedActivityService.java
+++ b/service/src/com/android/car/am/FixedActivityService.java
@@ -138,7 +138,7 @@
         }
         if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) {
             synchronized (FixedActivityService.this.mLock) {
-                FixedActivityService.this.mRunningActivities.clear();
+                clearRunningActivitiesLocked();
             }
         }
     };
@@ -290,6 +290,25 @@
         synchronized (mLock) {
             writer.println("mRunningActivities:" + mRunningActivities
                     + " ,mEventMonitoringActive:" + mEventMonitoringActive);
+            writer.println("mBlockingPresentations:");
+            for (int i = 0; i < mBlockingPresentations.size(); i++) {
+                Presentation p = mBlockingPresentations.valueAt(i);
+                if (p == null) {
+                    continue;
+                }
+                writer.println("display:" + mBlockingPresentations.keyAt(i)
+                        + " showing:" + p.isShowing());
+            }
+        }
+    }
+
+    private void clearRunningActivitiesLocked() {
+        int currentUser = ActivityManager.getCurrentUser();
+        for (int i = mRunningActivities.size() - 1; i >= 0; i--) {
+            RunningActivityInfo info = mRunningActivities.valueAt(i);
+            if (info == null || info.userId != currentUser) {
+                mRunningActivities.removeAt(i);
+            }
         }
     }
 
@@ -411,10 +430,15 @@
                         Presentation p = new Presentation(mContext, display,
                                 android.R.style.Theme_Black_NoTitleBar_Fullscreen);
                         p.setContentView(R.layout.activity_continuous_blank);
-                        p.show();
                         synchronized (mLock) {
+                            RunningActivityInfo info = mRunningActivities.get(displayIdForActivity);
+                            if (info != null && info.userId == ActivityManager.getCurrentUser()) {
+                                Log.i(TAG_AM, "Do not show Presentation, new req already made");
+                                return;
+                            }
                             mBlockingPresentations.append(displayIdForActivity, p);
                         }
+                        p.show();
                     });
                 }
                 mRunningActivities.removeAt(i);
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index d8ac6ac..94f31d2 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -101,8 +101,6 @@
 
     private final String mRenderingServiceConfig;
 
-
-
     @GuardedBy("mLock")
     private IInstrumentClusterNavigation mIInstrumentClusterNavigationFromRenderer;
 
@@ -111,6 +109,7 @@
         @Override
         public void onNavigationStateChanged(Bundle bundle) {
             ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_NAVIGATION_MANAGER);
+            assertNavigationFocus();
             // No retry here as new events will be sent later.
             IInstrumentClusterNavigation navigationBinder = getNavigationBinder(
                     /* retryOnFail= */ false);
@@ -323,6 +322,23 @@
         changeNavContextOwner(appType, uid, pid, false);
     }
 
+    private void assertNavigationFocus() {
+        int uid = Binder.getCallingUid();
+        int pid = Binder.getCallingPid();
+        synchronized (mLock) {
+            if (uid == mNavContextOwner.uid && pid == mNavContextOwner.pid) {
+                return;
+            }
+        }
+        // Stored one failed. There can be a delay, so check with real one again.
+        AppFocusService afs = CarLocalServices.getService(AppFocusService.class);
+        if (afs != null && afs.isFocusOwner(uid, pid,
+                CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION)) {
+            return;
+        }
+        throw new IllegalStateException("Client not owning APP_FOCUS_TYPE_NAVIGATION");
+    }
+
     private void changeNavContextOwner(int appType, int uid, int pid, boolean acquire) {
         if (appType != CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION) {
             return;
diff --git a/service/src/com/android/car/hal/UserHalService.java b/service/src/com/android/car/hal/UserHalService.java
index f995047..9abe92b 100644
--- a/service/src/com/android/car/hal/UserHalService.java
+++ b/service/src/com/android/car/hal/UserHalService.java
@@ -29,11 +29,9 @@
 import android.car.userlib.HalCallback;
 import android.car.userlib.UserHalHelper;
 import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponse;
-import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
 import android.hardware.automotive.vehicle.V2_0.SwitchUserMessageType;
 import android.hardware.automotive.vehicle.V2_0.SwitchUserResponse;
 import android.hardware.automotive.vehicle.V2_0.SwitchUserStatus;
-import android.hardware.automotive.vehicle.V2_0.UserFlags;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationType;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationGetRequest;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationResponse;
@@ -45,7 +43,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.ServiceSpecificException;
-import android.os.UserHandle;
 import android.sysprop.CarProperties;
 import android.text.TextUtils;
 import android.util.EventLog;
@@ -53,6 +50,8 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
+import com.android.car.CarLocalServices;
+import com.android.car.user.CarUserService;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.car.EventLogTags;
@@ -95,7 +94,7 @@
     // This handler handles 2 types of messages:
     // - "Anonymous" messages (what=0) containing runnables.
     // - "Identifiable" messages used to check for timeouts (whose 'what' is the request id).
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Handler mHandler;
 
     /**
      * Value used on the next request.
@@ -110,7 +109,13 @@
     private final SparseArray<PendingRequest<?, ?>> mPendingRequests = new SparseArray<>();
 
     public UserHalService(VehicleHal hal) {
+        this(hal, new Handler(Looper.getMainLooper()));
+    }
+
+    @VisibleForTesting
+    UserHalService(VehicleHal hal, Handler handler) {
         mHal = hal;
+        mHandler = handler;
     }
 
     @Override
@@ -336,7 +341,7 @@
         VehiclePropValue propRequest;
         synchronized (mLock) {
             checkSupportedLocked();
-            int requestId = mNextRequestId++;
+            int requestId = getNextRequestId();
             EventLog.writeEvent(EventLogTags.CAR_USER_HAL_LEGACY_SWITCH_USER_REQ, requestId,
                     targetInfo.userId, usersInfo.currentUser.userId);
             propRequest = getPropRequestForSwitchUserLocked(requestId,
@@ -604,7 +609,7 @@
     }
 
     private void handleCheckIfRequestTimedOut(int requestId) {
-        PendingRequest<?, ?> pendingRequest = getPendingResponse(requestId);
+        PendingRequest<?, ?> pendingRequest = getPendingRequest(requestId);
         if (pendingRequest == null) return;
 
         Log.w(TAG, "Request #" + requestId + " timed out");
@@ -613,7 +618,7 @@
     }
 
     @Nullable
-    private PendingRequest<?, ?> getPendingResponse(int requestId) {
+    private PendingRequest<?, ?> getPendingRequest(int requestId) {
         synchronized (mLock) {
             return mPendingRequests.get(requestId);
         }
@@ -631,41 +636,73 @@
             return;
         }
         handleRemovePendingRequest(requestId);
-        InitialUserInfoResponse response = new InitialUserInfoResponse();
-        // TODO(b/150413515): use helper method to convert prop value to proper response
-        response.requestId = requestId;
-        response.action = value.value.int32Values.get(1);
-        switch (response.action) {
-            case InitialUserInfoResponseAction.DEFAULT:
-                response.userToSwitchOrCreate.userId = UserHandle.USER_NULL;
-                response.userToSwitchOrCreate.flags = UserFlags.NONE;
-                break;
-            case InitialUserInfoResponseAction.SWITCH:
-                response.userToSwitchOrCreate.userId = value.value.int32Values.get(2);
-                response.userToSwitchOrCreate.flags = UserFlags.NONE;
-                break;
-            case InitialUserInfoResponseAction.CREATE:
-                response.userToSwitchOrCreate.userId = UserHandle.USER_NULL;
-                response.userToSwitchOrCreate.flags = value.value.int32Values.get(2);
-                response.userNameToCreate = value.value.stringValue;
-                break;
-            default:
-                Log.e(TAG, "invalid action (" + response.action + ") from HAL: " + value);
-                EventLog.writeEvent(EventLogTags.CAR_USER_HAL_INITIAL_USER_INFO_RESP, requestId,
-                        HalCallback.STATUS_WRONG_HAL_RESPONSE);
-                callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, null);
-                return;
+
+        InitialUserInfoResponse response;
+        try {
+            response = UserHalHelper.toInitialUserInfoResponse(value);
+        } catch (RuntimeException e) {
+            Log.e(TAG, "invalid response (" + value + ") from HAL", e);
+            EventLog.writeEvent(EventLogTags.CAR_USER_HAL_INITIAL_USER_INFO_RESP, requestId,
+                    HalCallback.STATUS_WRONG_HAL_RESPONSE);
+            callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, null);
+            return;
         }
         EventLog.writeEvent(EventLogTags.CAR_USER_HAL_INITIAL_USER_INFO_RESP, requestId,
                 HalCallback.STATUS_OK, response.action,
                 response.userToSwitchOrCreate.userId, response.userToSwitchOrCreate.flags,
-                response.userNameToCreate);
+                response.userNameToCreate, response.userLocales);
         if (DBG) Log.d(TAG, "replying to request " + requestId + " with " + response);
         callback.onResponse(HalCallback.STATUS_OK, response);
     }
 
     private void handleOnSwitchUserResponse(VehiclePropValue value) {
         int requestId = value.value.int32Values.get(0);
+        int messageType = value.value.int32Values.get(1);
+
+        if (messageType == SwitchUserMessageType.VEHICLE_RESPONSE) {
+            handleVehicleResponse(value);
+            return;
+        }
+
+        if (messageType == SwitchUserMessageType.VEHICLE_REQUEST) {
+            handleVehicleRequest(value);
+            return;
+        }
+
+        Log.e(TAG, "handleOnSwitchUserResponse invalid message type (" + messageType
+                + ") from HAL: " + value);
+
+        // check if a callback exists for the request ID
+        HalCallback<SwitchUserResponse> callback =
+                handleGetPendingCallback(requestId, SwitchUserResponse.class);
+        if (callback != null) {
+            handleRemovePendingRequest(requestId);
+            EventLog.writeEvent(EventLogTags.CAR_USER_HAL_SWITCH_USER_RESP, requestId,
+                    HalCallback.STATUS_WRONG_HAL_RESPONSE, SwitchUserStatus.FAILURE);
+            callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, null);
+            return;
+        }
+    }
+
+    private void handleVehicleRequest(VehiclePropValue value) {
+        int requestId = value.value.int32Values.get(0);
+        // Index 1 is message type, which is not required in this call.
+        int targetUserId = value.value.int32Values.get(2);
+        EventLog.writeEvent(EventLogTags.CAR_USER_HAL_OEM_SWITCH_USER_REQ, requestId, targetUserId);
+
+        // HAL vehicle request should have negative request ID
+        if (requestId >= 0) {
+            Log.e(TAG, "handleVehicleRequest invalid requestId (" + requestId + ") from HAL: "
+                    + value);
+            return;
+        }
+
+        CarUserService userService = CarLocalServices.getService(CarUserService.class);
+        userService.switchAndroidUserFromHal(requestId, targetUserId);
+    }
+
+    private void handleVehicleResponse(VehiclePropValue value) {
+        int requestId = value.value.int32Values.get(0);
         HalCallback<SwitchUserResponse> callback =
                 handleGetPendingCallback(requestId, SwitchUserResponse.class);
         if (callback == null) {
@@ -678,13 +715,6 @@
         SwitchUserResponse response = new SwitchUserResponse();
         response.requestId = requestId;
         response.messageType = value.value.int32Values.get(1);
-        if (response.messageType != SwitchUserMessageType.VEHICLE_RESPONSE) {
-            EventLog.writeEvent(EventLogTags.CAR_USER_HAL_SWITCH_USER_RESP, requestId,
-                    HalCallback.STATUS_WRONG_HAL_RESPONSE);
-            Log.e(TAG, "invalid message type (" + response.messageType + ") from HAL: " + value);
-            callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, null);
-            return;
-        }
         response.status = value.value.int32Values.get(2);
         if (response.status == SwitchUserStatus.SUCCESS
                 || response.status == SwitchUserStatus.FAILURE) {
@@ -703,7 +733,7 @@
     }
 
     private <T> HalCallback<T> handleGetPendingCallback(int requestId, Class<T> clazz) {
-        PendingRequest<?, ?> pendingRequest = getPendingResponse(requestId);
+        PendingRequest<?, ?> pendingRequest = getPendingRequest(requestId);
         if (pendingRequest == null) return null;
 
         if (pendingRequest.responseClass != clazz) {
diff --git a/service/src/com/android/car/hal/VehicleHal.java b/service/src/com/android/car/hal/VehicleHal.java
index 7e80e74..8706c00 100644
--- a/service/src/com/android/car/hal/VehicleHal.java
+++ b/service/src/com/android/car/hal/VehicleHal.java
@@ -60,6 +60,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
@@ -712,14 +713,16 @@
      * @param property the Vehicle property Id as defined in the HAL
      * @param zone     Zone that this event services
      * @param value    Data value of the event
+     * @param delayTime Add a certain duration to event timestamp
      */
-    public void injectVhalEvent(String property, String zone, String value)
+    public void injectVhalEvent(String property, String zone, String value, String delayTime)
             throws NumberFormatException {
         if (value == null || zone == null || property == null) {
             return;
         }
         int propId = Integer.decode(property);
         int zoneId = Integer.decode(zone);
+        int duration = Integer.decode(delayTime);
         VehiclePropValue v = createPropValue(propId, zoneId);
         int propertyType = propId & VehiclePropertyType.MASK;
         // Values can be comma separated list
@@ -745,7 +748,7 @@
                 Log.e(CarLog.TAG_HAL, "Property type unsupported:" + propertyType);
                 return;
         }
-        v.timestamp = SystemClock.elapsedRealtimeNanos();
+        v.timestamp = SystemClock.elapsedRealtimeNanos() + TimeUnit.SECONDS.toNanos(duration);
         onPropertyEvent(Lists.newArrayList(v));
     }
 
diff --git a/service/src/com/android/car/user/CarUserService.java b/service/src/com/android/car/user/CarUserService.java
index 2ae56cb..d7de501 100644
--- a/service/src/com/android/car/user/CarUserService.java
+++ b/service/src/com/android/car/user/CarUserService.java
@@ -33,13 +33,17 @@
 import android.car.user.CarUserManager.UserLifecycleEvent;
 import android.car.user.CarUserManager.UserLifecycleEventType;
 import android.car.user.CarUserManager.UserLifecycleListener;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
 import android.car.user.UserSwitchResult;
 import android.car.userlib.CarUserManagerHelper;
 import android.car.userlib.CommonConstants.CarUserServiceConstants;
 import android.car.userlib.HalCallback;
 import android.car.userlib.UserHalHelper;
+import android.car.userlib.UserHelper;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -48,6 +52,8 @@
 import android.hardware.automotive.vehicle.V2_0.SwitchUserStatus;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationGetRequest;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationResponse;
+import android.hardware.automotive.vehicle.V2_0.UserIdentificationSetAssociation;
+import android.hardware.automotive.vehicle.V2_0.UserIdentificationSetRequest;
 import android.hardware.automotive.vehicle.V2_0.UsersInfo;
 import android.location.LocationManager;
 import android.os.Binder;
@@ -60,6 +66,7 @@
 import android.os.UserManager;
 import android.provider.Settings;
 import android.sysprop.CarProperties;
+import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
 import android.util.SparseArray;
@@ -109,7 +116,14 @@
     public static final String BUNDLE_USER_FLAGS = CarUserServiceConstants.BUNDLE_USER_FLAGS;
     /** {@code String} extra used to represent a user name in a {@link IResultReceiver} response. */
     public static final String BUNDLE_USER_NAME = CarUserServiceConstants.BUNDLE_USER_NAME;
-    /** {@code int} extra used to represent the info action {@link IResultReceiver} response. */
+    /**
+     * {@code int} extra used to represent the user locales in a {@link IResultReceiver} response.
+     */
+    public static final String BUNDLE_USER_LOCALES =
+            CarUserServiceConstants.BUNDLE_USER_LOCALES;
+    /**
+     * {@code int} extra used to represent the info action in a {@link IResultReceiver} response.
+     */
     public static final String BUNDLE_INITIAL_INFO_ACTION =
             CarUserServiceConstants.BUNDLE_INITIAL_INFO_ACTION;
 
@@ -292,6 +306,7 @@
         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());
 
         dumpUserMetrics(writer);
     }
@@ -412,29 +427,21 @@
      * @see CarUserManager.switchDriver
      */
     @Override
-    public boolean switchDriver(@UserIdInt int driverId) {
+    public void switchDriver(@UserIdInt int driverId, AndroidFuture<UserSwitchResult> receiver) {
         checkManageUsersPermission("switchDriver");
-        if (driverId == UserHandle.USER_SYSTEM && UserManager.isHeadlessSystemUserMode()) {
+        if (UserHelper.isHeadlessSystemUser(driverId)) {
             // System user doesn't associate with real person, can not be switched to.
             Log.w(TAG_USER, "switching to system user in headless system user mode is not allowed");
-            return false;
+            sendResult(receiver, UserSwitchResult.STATUS_INVALID_REQUEST);
+            return;
         }
         int userSwitchable = mUserManager.getUserSwitchability();
         if (userSwitchable != UserManager.SWITCHABILITY_STATUS_OK) {
             Log.w(TAG_USER, "current process is not allowed to switch user");
-            return false;
+            sendResult(receiver, UserSwitchResult.STATUS_INVALID_REQUEST);
+            return;
         }
-        if (driverId == ActivityManager.getCurrentUser()) {
-            // The current user is already the given user.
-            return true;
-        }
-        try {
-            return mAm.switchUser(driverId);
-        } catch (RemoteException e) {
-            // ignore
-            Log.w(TAG_USER, "error while switching user", e);
-        }
-        return false;
+        switchUser(driverId, mHalTimeoutMs, receiver);
     }
 
     /**
@@ -446,7 +453,7 @@
     @NonNull
     public List<UserInfo> getAllDrivers() {
         checkManageUsersOrDumpPermission("getAllDrivers");
-        return getUsers((user) -> !isSystemUser(user.id) && user.isEnabled()
+        return getUsers((user) -> !UserHelper.isHeadlessSystemUser(user.id) && user.isEnabled()
                 && !user.isManagedProfile() && !user.isEphemeral());
     }
 
@@ -461,8 +468,8 @@
     public List<UserInfo> getPassengers(@UserIdInt int driverId) {
         checkManageUsersOrDumpPermission("getPassengers");
         return getUsers((user) -> {
-            return !isSystemUser(user.id) && user.isEnabled() && user.isManagedProfile()
-                    && user.profileGroupId == driverId;
+            return !UserHelper.isHeadlessSystemUser(user.id) && user.isEnabled()
+                    && user.isManagedProfile() && user.profileGroupId == driverId;
         });
     }
 
@@ -596,7 +603,7 @@
             if (resp != null) {
                 EventLog.writeEvent(EventLogTags.CAR_USER_SVC_INITIAL_USER_INFO_RESP,
                         status, resp.action, resp.userToSwitchOrCreate.userId,
-                        resp.userToSwitchOrCreate.flags, resp.userNameToCreate);
+                        resp.userToSwitchOrCreate.flags, resp.userNameToCreate, resp.userLocales);
                 switch (resp.action) {
                     case InitialUserInfoResponseAction.SWITCH:
                         resultData = new Bundle();
@@ -620,6 +627,10 @@
             } else {
                 EventLog.writeEvent(EventLogTags.CAR_USER_SVC_INITIAL_USER_INFO_RESP, status);
             }
+            if (resultData != null && !TextUtils.isEmpty(resp.userLocales)) {
+                resultData.putString(BUNDLE_USER_LOCALES, resp.userLocales);
+            }
+
             sendResult(receiver, status, resultData);
         });
     }
@@ -867,7 +878,7 @@
     }
 
     @Override
-    public GetUserIdentificationAssociationResponse getUserIdentificationAssociation(int[] types) {
+    public UserIdentificationAssociationResponse getUserIdentificationAssociation(int[] types) {
         Preconditions.checkArgument(!ArrayUtils.isEmpty(types), "must have at least one type");
         checkManageUsersPermission("getUserIdentificationAssociation");
 
@@ -888,7 +899,7 @@
         if (halResponse == null) {
             Log.w(TAG, "getUserIdentificationAssociation(): HAL returned null for "
                     + Arrays.toString(types));
-            return null;
+            return UserIdentificationAssociationResponse.forFailure();
         }
 
         int[] values = new int[halResponse.associations.size()];
@@ -897,7 +908,68 @@
         }
         EventLog.writeEvent(EventLogTags.CAR_USER_MGR_GET_USER_AUTH_RESP, values.length);
 
-        return new GetUserIdentificationAssociationResponse(halResponse.errorMessage, values);
+        return UserIdentificationAssociationResponse.forSuccess(values, halResponse.errorMessage);
+    }
+
+    @Override
+    public void setUserIdentificationAssociation(int timeoutMs, int[] types, int[] values,
+            AndroidFuture<UserIdentificationAssociationResponse> result) {
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(types), "must have at least one type");
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(values), "must have at least one value");
+        if (types.length != values.length) {
+            throw new IllegalArgumentException("types (" + Arrays.toString(types) + ") and values ("
+                    + Arrays.toString(values) + ") should have the same length");
+        }
+        checkManageUsersPermission("setUserIdentificationAssociation");
+
+        int uid = getCallingUid();
+        int userId = UserHandle.getUserId(uid);
+        EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_REQ, uid, userId, types.length);
+
+        UserIdentificationSetRequest request = new UserIdentificationSetRequest();
+        request.userInfo.userId = userId;
+        request.userInfo.flags = getHalUserInfoFlags(userId);
+
+        request.numberAssociations = types.length;
+        for (int i = 0; i < types.length; i++) {
+            UserIdentificationSetAssociation association = new UserIdentificationSetAssociation();
+            association.type = types[i];
+            association.value = values[i];
+            request.associations.add(association);
+        }
+
+        mHal.setUserAssociation(timeoutMs, request, (status, resp) -> {
+            if (status != HalCallback.STATUS_OK) {
+                Log.w(TAG, "setUserIdentificationAssociation(): invalid callback status ("
+                        + UserHalHelper.halCallbackStatusToString(status) + ") for response "
+                        + resp);
+                if (resp == null || TextUtils.isEmpty(resp.errorMessage)) {
+                    EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_RESP, 0);
+                    result.complete(UserIdentificationAssociationResponse.forFailure());
+                    return;
+                }
+                EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_RESP, 0,
+                        resp.errorMessage);
+                result.complete(
+                        UserIdentificationAssociationResponse.forFailure(resp.errorMessage));
+                return;
+            }
+            int respSize = resp.associations.size();
+            EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_RESP, respSize,
+                    resp.errorMessage);
+
+            int[] responseTypes = new int[respSize];
+            for (int i = 0; i < respSize; i++) {
+                responseTypes[i] = resp.associations.get(i).value;
+            }
+            UserIdentificationAssociationResponse response = UserIdentificationAssociationResponse
+                    .forSuccess(responseTypes, resp.errorMessage);
+            if (Log.isLoggable(TAG_USER, Log.DEBUG)) {
+                Log.d(TAG, "setUserIdentificationAssociation(): resp= " + resp
+                        + ", converted=" + response);
+            }
+            result.complete(response);
+        });
     }
 
     /**
@@ -931,6 +1003,49 @@
         receiver.complete(new UserSwitchResult(status, errorMessage));
     }
 
+    /**
+     * Calls activity manager for user switch.
+     *
+     * <p><b>NOTE</b> This method is meant to be called just by UserHalService.
+     *
+     * @param requestId for the user switch request
+     * @param targetUserId of the target user
+     *
+     * @hide
+     */
+    public void switchAndroidUserFromHal(int requestId, @UserIdInt int targetUserId) {
+        EventLog.writeEvent(EventLogTags.CAR_USER_SVC_SWITCH_USER_FROM_HAL_REQ, requestId,
+                targetUserId);
+        Log.i(TAG_USER, "User hal requested a user switch. Target user id " + targetUserId);
+
+        try {
+            boolean result = mAm.switchUser(targetUserId);
+            if (result) {
+                updateUserSwitchInProcess(requestId, targetUserId);
+            } else {
+                postSwitchHalResponse(requestId, targetUserId);
+            }
+        } catch (RemoteException e) {
+            // ignore
+            Log.w(TAG_USER, "error while switching user " + targetUserId, e);
+        }
+    }
+
+    private void updateUserSwitchInProcess(int requestId, @UserIdInt int targetUserId) {
+        synchronized (mLockUser) {
+            if (mUserIdForUserSwitchInProcess != UserHandle.USER_NULL) {
+                // Some other user switch is in process.
+                if (Log.isLoggable(TAG_USER, Log.DEBUG)) {
+                    Log.d(TAG_USER, "User switch for user: " + mUserIdForUserSwitchInProcess
+                            + " is in process. Abandoning it as a new user switch is requested"
+                            + " for the target user: " + targetUserId);
+                }
+            }
+            mUserIdForUserSwitchInProcess = targetUserId;
+            mRequestIdForUserSwitchInProcess = requestId;
+        }
+    }
+
     private void postSwitchHalResponse(int requestId, @UserIdInt int targetUserId) {
         UserInfo targetUser = mUserManager.getUserInfo(targetUserId);
         UsersInfo usersInfo = getUsersInfo();
@@ -959,11 +1074,45 @@
      */
     @Override
     public void setUserSwitchUiCallback(@NonNull IResultReceiver receiver) {
-        // TODO(b/154958003): check UID, only carSysUI should be allowed to set it.
+        checkManageUsersPermission("setUserSwitchUiCallback");
+
+        // Confirm that caller is system UI.
+        String systemUiPackageName = getSystemUiPackageName();
+        if (systemUiPackageName == null) {
+            throw new IllegalStateException("System UI package not found.");
+        }
+
+        try {
+            int systemUiUid = mContext
+                    .createContextAsUser(UserHandle.SYSTEM, /* flags= */ 0).getPackageManager()
+                    .getPackageUid(systemUiPackageName, PackageManager.MATCH_SYSTEM_ONLY);
+            int callerUid = Binder.getCallingUid();
+            if (systemUiUid != callerUid) {
+                throw new SecurityException("Invalid caller. Only" + systemUiPackageName
+                        + " is allowed to make this call");
+            }
+        } catch (NameNotFoundException e) {
+            throw new IllegalStateException("Package " + systemUiPackageName + " not found.");
+        }
+
         mUserSwitchUiReceiver = receiver;
     }
 
-    // TODO(b/144120654): use helper to generate UsersInfo
+    // TODO(157082995): This information can be taken from
+    // PackageManageInternalImpl.getSystemUiServiceComponent
+    @Nullable
+    private String getSystemUiPackageName() {
+        try {
+            ComponentName componentName = ComponentName.unflattenFromString(mContext.getResources()
+                    .getString(com.android.internal.R.string.config_systemUIServiceComponent));
+            return componentName.getPackageName();
+        } catch (RuntimeException e) {
+            Log.w(TAG_USER, "error while getting system UI package name.", e);
+            return null;
+        }
+    }
+
+    // TODO(b/150413515): use helper to generate UsersInfo
     private UsersInfo getUsersInfo() {
         UserInfo currentUser;
         try {
@@ -975,7 +1124,7 @@
         return getUsersInfo(currentUser);
     }
 
-    // TODO(b/144120654): use helper to generate UsersInfo
+    // TODO(b/150413515): use helper to generate UsersInfo
     private UsersInfo getUsersInfo(@NonNull UserInfo currentUser) {
         List<UserInfo> existingUsers = mUserManager.getUsers();
         int size = existingUsers.size();
@@ -997,11 +1146,6 @@
         return usersInfo;
     }
 
-    /** Returns whether the given user is a system user. */
-    private static boolean isSystemUser(@UserIdInt int userId) {
-        return userId == UserHandle.USER_SYSTEM;
-    }
-
     private void updateDefaultUserRestriction() {
         // We want to set restrictions on system and guest users only once. These are persisted
         // onto disk, so it's sufficient to do it once + we minimize the number of disk writes.
@@ -1296,14 +1440,14 @@
     }
 
     private void onUserSwitching(@UserIdInt int fromUserId, @UserIdInt int toUserId) {
-        Log.i(TAG_USER, "onSwitchUser() callback for user " + toUserId);
+        Log.i(TAG_USER, "onUserSwitching() callback for user " + toUserId);
         TimingsTraceLog t = new TimingsTraceLog(TAG_USER, Trace.TRACE_TAG_SYSTEM_SERVER);
         t.traceBegin("onUserSwitching-" + toUserId);
 
         // Switch HAL users if user switch is not requested by CarUserService
         notifyHalLegacySwitch(fromUserId, toUserId);
 
-        if (!isSystemUser(toUserId)) {
+        if (!UserHelper.isHeadlessSystemUser(toUserId)) {
             mCarUserManagerHelper.setLastActiveUser(toUserId);
         }
         if (mLastPassengerId != UserHandle.USER_NULL) {
diff --git a/service/src/com/android/car/user/UserMetrics.java b/service/src/com/android/car/user/UserMetrics.java
index adba813..62b9755 100644
--- a/service/src/com/android/car/user/UserMetrics.java
+++ b/service/src/com/android/car/user/UserMetrics.java
@@ -65,7 +65,7 @@
     // garage mode
     private static final int INITIAL_CAPACITY = 2;
 
-    // TODO(b/144120654): read from resources
+    // TODO(b/150413515): read from resources
     private static final int LOG_SIZE = 10;
 
     private final Object mLock = new Object();
diff --git a/service/src/com/android/car/vms/VmsBrokerService.java b/service/src/com/android/car/vms/VmsBrokerService.java
index 9ecfc3e..31c60bd 100644
--- a/service/src/com/android/car/vms/VmsBrokerService.java
+++ b/service/src/com/android/car/vms/VmsBrokerService.java
@@ -41,8 +41,6 @@
 import android.util.Log;
 
 import com.android.car.CarServiceBase;
-import com.android.car.VmsLayersAvailability;
-import com.android.car.VmsPublishersInfo;
 import com.android.car.stats.CarStatsService;
 import com.android.car.stats.VmsClientLogger;
 import com.android.internal.annotations.GuardedBy;
@@ -53,6 +51,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -74,8 +73,8 @@
     private final CarStatsService mStatsService;
     private final IntSupplier mGetCallingUid;
 
-    private final VmsPublishersInfo mPublishersInfo = new VmsPublishersInfo();
-    private final VmsLayersAvailability mAvailableLayers = new VmsLayersAvailability();
+    private final VmsProviderInfoStore mProviderInfoStore = new VmsProviderInfoStore();
+    private final VmsLayerAvailability mAvailableLayers = new VmsLayerAvailability();
 
     private final Object mLock = new Object();
     @GuardedBy("mLock")
@@ -111,7 +110,17 @@
 
     @Override
     public void dump(PrintWriter writer) {
-        // TODO(b/149125079): Implement dumpsys
+        writer.println("*" + TAG + "*");
+        synchronized (mLock) {
+            writer.println("mAvailableLayers: " + mAvailableLayers.getAvailableLayers());
+            writer.println();
+            writer.println("mSubscriptionState: " + mSubscriptionState);
+            writer.println();
+            writer.println("mClientMap:");
+            mClientMap.values().stream()
+                    .sorted(Comparator.comparingInt(VmsClientInfo::getUid))
+                    .forEach(client -> client.dump(writer, "  "));
+        }
     }
 
     @Override
@@ -125,18 +134,20 @@
         mStatsService.getVmsClientLogger(clientUid)
                 .logConnectionState(VmsClientLogger.ConnectionState.CONNECTED);
 
+        IBinder.DeathRecipient deathRecipient;
+        try {
+            deathRecipient = () -> unregisterClient(clientToken,
+                    VmsClientLogger.ConnectionState.DISCONNECTED);
+            callback.asBinder().linkToDeath(deathRecipient, 0);
+        } catch (RemoteException e) {
+            mStatsService.getVmsClientLogger(clientUid)
+                    .logConnectionState(VmsClientLogger.ConnectionState.DISCONNECTED);
+            throw new IllegalStateException("Client callback is already dead");
+        }
+
         synchronized (mLock) {
-            try {
-                callback.asBinder().linkToDeath(
-                        () -> unregisterClient(clientToken,
-                                VmsClientLogger.ConnectionState.DISCONNECTED), 0);
-                mClientMap.put(clientToken, new VmsClientInfo(clientUid, clientPackage, callback,
-                        legacyClient));
-            } catch (RemoteException e) {
-                Log.w(TAG, "Client process is already dead", e);
-                mStatsService.getVmsClientLogger(clientUid)
-                        .logConnectionState(VmsClientLogger.ConnectionState.DISCONNECTED);
-            }
+            mClientMap.put(clientToken, new VmsClientInfo(clientUid, clientPackage, callback,
+                    legacyClient, deathRecipient));
             return new VmsRegistrationInfo(
                     mAvailableLayers.getAvailableLayers(),
                     mSubscriptionState);
@@ -153,7 +164,7 @@
     public VmsProviderInfo getProviderInfo(IBinder clientToken, int providerId) {
         assertAnyVmsPermission(mContext);
         getClient(clientToken); // Assert that the client is registered
-        return new VmsProviderInfo(mPublishersInfo.getPublisherInfoOrNull(providerId));
+        return new VmsProviderInfo(mProviderInfoStore.getProviderInfo(providerId));
     }
 
     @Override
@@ -172,12 +183,13 @@
     @Override
     public int registerProvider(IBinder clientToken, VmsProviderInfo providerInfo) {
         assertVmsPublisherPermission(mContext);
+        VmsClientInfo client = getClient(clientToken);
+        int providerId;
         synchronized (mLock) {
-            VmsClientInfo client = getClient(clientToken);
-            int publisherId = mPublishersInfo.getIdForInfo(providerInfo.getDescription());
-            client.addProviderId(publisherId);
-            return publisherId;
+            providerId = mProviderInfoStore.getProviderId(providerInfo.getDescription());
         }
+        client.addProviderId(providerId);
+        return providerId;
     }
 
     @Override
@@ -252,15 +264,17 @@
     }
 
     private void unregisterClient(IBinder clientToken, int connectionState) {
+        VmsClientInfo client;
         synchronized (mLock) {
-            VmsClientInfo client = mClientMap.remove(clientToken);
-            if (client != null) {
-                mStatsService.getVmsClientLogger(client.getUid())
-                        .logConnectionState(connectionState);
-            }
+            client = mClientMap.remove(clientToken);
         }
-        updateAvailableLayers();
-        updateSubscriptionState();
+        if (client != null) {
+            client.getCallback().asBinder().unlinkToDeath(client.getDeathRecipient(), 0);
+            mStatsService.getVmsClientLogger(client.getUid())
+                    .logConnectionState(connectionState);
+            updateAvailableLayers();
+            updateSubscriptionState();
+        }
     }
 
     private VmsClientInfo getClient(IBinder clientToken) {
diff --git a/service/src/com/android/car/vms/VmsClientInfo.java b/service/src/com/android/car/vms/VmsClientInfo.java
index 05a11dd..d64b6b7 100644
--- a/service/src/com/android/car/vms/VmsClientInfo.java
+++ b/service/src/com/android/car/vms/VmsClientInfo.java
@@ -21,12 +21,14 @@
 import android.car.vms.VmsLayer;
 import android.car.vms.VmsLayerDependency;
 import android.car.vms.VmsLayersOffering;
+import android.os.IBinder;
 import android.util.ArraySet;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.GuardedBy;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -43,6 +45,7 @@
     private final String mPackageName;
     private final IVmsClientCallback mCallback;
     private final boolean mLegacyClient;
+    private final IBinder.DeathRecipient mDeathRecipient;
 
     private final Object mLock = new Object();
     @GuardedBy("mLock")
@@ -58,11 +61,13 @@
     @GuardedBy("mLock")
     private boolean mMonitoringEnabled;
 
-    VmsClientInfo(int uid, String packageName, IVmsClientCallback callback, boolean legacyClient) {
+    VmsClientInfo(int uid, String packageName, IVmsClientCallback callback, boolean legacyClient,
+            IBinder.DeathRecipient deathRecipient) {
         mUid = uid;
         mPackageName = packageName;
         mCallback = callback;
         mLegacyClient = legacyClient;
+        mDeathRecipient = deathRecipient;
     }
 
     int getUid() {
@@ -77,6 +82,14 @@
         return mCallback;
     }
 
+    boolean isLegacyClient() {
+        return mLegacyClient;
+    }
+
+    IBinder.DeathRecipient getDeathRecipient() {
+        return mDeathRecipient;
+    }
+
     void addProviderId(int providerId) {
         synchronized (mLock) {
             mProviderIds.put(providerId, true);
@@ -168,8 +181,50 @@
         }
     }
 
-    boolean isLegacyClient() {
-        return mLegacyClient;
+    void dump(PrintWriter writer, String indent) {
+        synchronized (mLock) {
+            String prefix = indent;
+            writer.println(prefix + "VmsClient [" + mPackageName + "]");
+
+            prefix = indent + "  ";
+            writer.println(prefix + "UID: " + mUid);
+            writer.println(prefix + "Legacy Client: " + mLegacyClient);
+            writer.println(prefix + "Monitoring: " + mMonitoringEnabled);
+
+            if (mProviderIds.size() > 0) {
+                writer.println(prefix + "Offerings:");
+                for (int i = 0; i < mProviderIds.size(); i++) {
+                    prefix = indent + "    ";
+                    int providerId = mProviderIds.keyAt(i);
+                    writer.println(prefix + "Provider [" + providerId + "]");
+
+                    for (VmsLayerDependency layerOffering : mOfferings.get(
+                            providerId, Collections.emptySet())) {
+                        prefix = indent + "      ";
+                        writer.println(prefix + layerOffering.getLayer());
+                        if (!layerOffering.getDependencies().isEmpty()) {
+                            prefix = indent + "        ";
+                            writer.println(prefix + "Dependencies: "
+                                    + layerOffering.getDependencies());
+                        }
+                    }
+                }
+            }
+
+            if (!mLayerSubscriptions.isEmpty() || !mLayerAndProviderSubscriptions.isEmpty()) {
+                prefix = indent + "  ";
+                writer.println(prefix + "Subscriptions:");
+
+                prefix = indent + "    ";
+                for (VmsLayer layer : mLayerSubscriptions) {
+                    writer.println(prefix + layer);
+                }
+                for (Map.Entry<VmsLayer, Set<Integer>> layerEntry :
+                        mLayerAndProviderSubscriptions.entrySet()) {
+                    writer.println(prefix + layerEntry.getKey() + ": " + layerEntry.getValue());
+                }
+            }
+        }
     }
 
     private static <K, V> Map<K, Set<V>> deepCopy(Map<K, Set<V>> original) {
diff --git a/service/src/com/android/car/VmsLayersAvailability.java b/service/src/com/android/car/vms/VmsLayerAvailability.java
similarity index 72%
rename from service/src/com/android/car/VmsLayersAvailability.java
rename to service/src/com/android/car/vms/VmsLayerAvailability.java
index a9bbc7a..3521336 100644
--- a/service/src/com/android/car/VmsLayersAvailability.java
+++ b/service/src/com/android/car/vms/VmsLayerAvailability.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
+ * 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
  *
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.car;
+package com.android.car.vms;
 
 import android.car.vms.VmsAssociatedLayer;
 import android.car.vms.VmsAvailableLayers;
@@ -35,26 +35,22 @@
 
 /**
  * Manages VMS availability for layers.
- * <p>
+ *
  * Each VMS publisher sets its layers offering which are a list of layers the publisher claims
  * it might publish. VmsLayersAvailability calculates from all the offering what are the
  * available layers.
- *
- * @hide
  */
 
-public class VmsLayersAvailability {
+class VmsLayerAvailability {
     private static final boolean DBG = false;
-    private static final String TAG = "VmsLayersAvailability";
+    private static final String TAG = VmsLayerAvailability.class.getSimpleName();
 
     private final Object mLock = new Object();
     @GuardedBy("mLock")
     private final Map<VmsLayer, Set<Set<VmsLayer>>> mPotentialLayersAndDependencies =
             new HashMap<>();
     @GuardedBy("mLock")
-    private Set<VmsAssociatedLayer> mAvailableAssociatedLayers = Collections.EMPTY_SET;
-    @GuardedBy("mLock")
-    private Set<VmsAssociatedLayer> mUnavailableAssociatedLayers = Collections.EMPTY_SET;
+    private Set<VmsAssociatedLayer> mAvailableAssociatedLayers = Collections.emptySet();
     @GuardedBy("mLock")
     private Map<VmsLayer, Set<Integer>> mPotentialLayersAndPublishers = new HashMap<>();
     @GuardedBy("mLock")
@@ -63,7 +59,7 @@
     /**
      * Setting the current layers offerings as reported by publishers.
      */
-    public void setPublishersOffering(Collection<VmsLayersOffering> publishersLayersOfferings) {
+    void setPublishersOffering(Collection<VmsLayersOffering> publishersLayersOfferings) {
         synchronized (mLock) {
             reset();
 
@@ -72,22 +68,12 @@
                     VmsLayer layer = dependency.getLayer();
 
                     // Associate publishers with layers.
-                    Set<Integer> curPotentialLayerAndPublishers =
-                            mPotentialLayersAndPublishers.get(layer);
-                    if (curPotentialLayerAndPublishers == null) {
-                        curPotentialLayerAndPublishers = new HashSet<>();
-                        mPotentialLayersAndPublishers.put(layer, curPotentialLayerAndPublishers);
-                    }
-                    curPotentialLayerAndPublishers.add(offering.getPublisherId());
+                    mPotentialLayersAndPublishers.computeIfAbsent(layer, k -> new HashSet<>())
+                            .add(offering.getPublisherId());
 
                     // Add dependencies for availability calculation.
-                    Set<Set<VmsLayer>> curDependencies =
-                            mPotentialLayersAndDependencies.get(layer);
-                    if (curDependencies == null) {
-                        curDependencies = new HashSet<>();
-                        mPotentialLayersAndDependencies.put(layer, curDependencies);
-                    }
-                    curDependencies.add(dependency.getDependencies());
+                    mPotentialLayersAndDependencies.computeIfAbsent(layer, k -> new HashSet<>())
+                            .add(dependency.getDependencies());
                 }
             }
             calculateLayers();
@@ -97,7 +83,7 @@
     /**
      * Returns a collection of all the layers which may be published.
      */
-    public VmsAvailableLayers getAvailableLayers() {
+    VmsAvailableLayers getAvailableLayers() {
         synchronized (mLock) {
             return new VmsAvailableLayers(mAvailableAssociatedLayers, mSeq);
         }
@@ -107,8 +93,7 @@
         synchronized (mLock) {
             mPotentialLayersAndDependencies.clear();
             mPotentialLayersAndPublishers.clear();
-            mAvailableAssociatedLayers = Collections.EMPTY_SET;
-            mUnavailableAssociatedLayers = Collections.EMPTY_SET;
+            mAvailableAssociatedLayers = Collections.emptySet();
             mSeq += 1;
         }
     }
@@ -127,14 +112,8 @@
             mAvailableAssociatedLayers = Collections.unmodifiableSet(
                     availableLayersSet
                             .stream()
-                            .map(l -> new VmsAssociatedLayer(l, mPotentialLayersAndPublishers.get(l)))
-                            .collect(Collectors.toSet()));
-
-            mUnavailableAssociatedLayers = Collections.unmodifiableSet(
-                    mPotentialLayersAndDependencies.keySet()
-                            .stream()
-                            .filter(l -> !availableLayersSet.contains(l))
-                            .map(l -> new VmsAssociatedLayer(l, mPotentialLayersAndPublishers.get(l)))
+                            .map(l -> new VmsAssociatedLayer(l,
+                                    mPotentialLayersAndPublishers.get(l)))
                             .collect(Collectors.toSet()));
         }
     }
@@ -188,6 +167,5 @@
                 return;
             }
         }
-        return;
     }
 }
diff --git a/service/src/com/android/car/vms/VmsProviderInfoStore.java b/service/src/com/android/car/vms/VmsProviderInfoStore.java
new file mode 100644
index 0000000..5a3894c
--- /dev/null
+++ b/service/src/com/android/car/vms/VmsProviderInfoStore.java
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+package com.android.car.vms;
+
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+class VmsProviderInfoStore {
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final ArrayMap<InfoWrapper, Integer> mProvidersIds = new ArrayMap<>();
+    @GuardedBy("mLock")
+    private final ArrayList<InfoWrapper> mProvidersInfo = new ArrayList<>();
+
+    private static class InfoWrapper {
+        private final byte[] mInfo;
+
+        InfoWrapper(byte[] info) {
+            mInfo = info;
+        }
+
+        public byte[] getInfo() {
+            return mInfo.clone();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof InfoWrapper)) {
+                return false;
+            }
+            InfoWrapper p = (InfoWrapper) o;
+            return Arrays.equals(this.mInfo, p.mInfo);
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(mInfo);
+        }
+    }
+
+    /**
+     * Retrieves the provider ID for the given provider information. If the provider information
+     * has not previously been seen, it will be assigned a new provider ID.
+     *
+     * @param providerInfo Provider information to query or register.
+     * @return Provider ID for the given provider information.
+     */
+    int getProviderId(byte[] providerInfo) {
+        Integer providerId;
+        InfoWrapper wrappedProviderInfo = new InfoWrapper(providerInfo);
+        synchronized (mLock) {
+            // Check if provider is already registered
+            providerId = mProvidersIds.get(wrappedProviderInfo);
+            if (providerId == null) {
+                // Add the new provider and assign it the next ID
+                mProvidersInfo.add(wrappedProviderInfo);
+                providerId = mProvidersInfo.size();
+                mProvidersIds.put(wrappedProviderInfo, providerId);
+            }
+        }
+        return providerId;
+    }
+
+    /**
+     * Returns the provider info associated with the given provider ID.
+     * @param providerId Provider ID to query.
+     * @return Provider info associated with the ID, or null if provider ID is unknown.
+     */
+    @Nullable
+    byte[] getProviderInfo(int providerId) {
+        synchronized (mLock) {
+            return providerId < 1 || providerId > mProvidersInfo.size()
+                    ? null
+                    : mProvidersInfo.get(providerId - 1).getInfo();
+        }
+    }
+}
diff --git a/service/src/com/android/car/watchdog/CarWatchdogService.java b/service/src/com/android/car/watchdog/CarWatchdogService.java
index 041ba29..8a1ebc7 100644
--- a/service/src/com/android/car/watchdog/CarWatchdogService.java
+++ b/service/src/com/android/car/watchdog/CarWatchdogService.java
@@ -68,7 +68,7 @@
  */
 public final class CarWatchdogService extends ICarWatchdogService.Stub implements CarServiceBase {
 
-    private static final boolean DEBUG = true; // STOPSHIP if true (b/151474489)
+    private static final boolean DEBUG = false; // STOPSHIP if true
     private static final String TAG = TAG_WATCHDOG;
     private static final int[] ALL_TIMEOUTS =
             { TIMEOUT_CRITICAL, TIMEOUT_MODERATE, TIMEOUT_NORMAL };
diff --git a/surround_view/service-impl/Android.bp b/surround_view/service-impl/Android.bp
index b4dac9f..8ae7bba 100644
--- a/surround_view/service-impl/Android.bp
+++ b/surround_view/service-impl/Android.bp
@@ -70,14 +70,17 @@
             srcs: ["lib/arm64/libcore_lib_shared.so"]
         },
         x86: {
-            srcs: ["lib/x86/libcore_lib.so"]
+            srcs: ["lib/x86/libcore_lib_shared.so"]
         },
         x86_64: {
-            srcs: ["lib/x86-64/libcore_lib.so"]
+            srcs: ["lib/x86-64/libcore_lib_shared.so"]
         },
     },
     shared_libs: [
-        "libEGL",
+	"libutils",
+	"libcutils",
+	"libbase",
+	"libEGL",
         "libGLESv2",
         "libGLESv3",
         "libc",
diff --git a/surround_view/service-impl/SurroundView2dSession.cpp b/surround_view/service-impl/SurroundView2dSession.cpp
index f1a789b..c94400a 100644
--- a/surround_view/service-impl/SurroundView2dSession.cpp
+++ b/surround_view/service-impl/SurroundView2dSession.cpp
@@ -345,8 +345,15 @@
     // description.
     mSurroundView = unique_ptr<SurroundView>(Create());
 
-    mSurroundView->SetStaticData(GetCameras(), Get2dParams(), Get3dParams(),
-                                 GetUndistortionScales(), GetBoundingBox());
+    SurroundViewStaticDataParams params =
+        SurroundViewStaticDataParams(GetCameras(),
+                                     Get2dParams(),
+                                     Get3dParams(),
+                                     GetUndistortionScales(),
+                                     GetBoundingBox(),
+                                     map<string, CarTexture>(),
+                                     map<string, CarPart>());
+    mSurroundView->SetStaticData(params);
 
     // TODO(b/150412555): remove after EVS camera is used
     mInputPointers = mSurroundView->ReadImages(
diff --git a/surround_view/service-impl/SurroundView3dSession.cpp b/surround_view/service-impl/SurroundView3dSession.cpp
index 0204b55..88913af 100644
--- a/surround_view/service-impl/SurroundView3dSession.cpp
+++ b/surround_view/service-impl/SurroundView3dSession.cpp
@@ -19,9 +19,11 @@
 #include <android/hardware_buffer.h>
 #include <android/hidl/memory/1.0/IMemory.h>
 #include <hidlmemory/mapping.h>
-#include <set>
 #include <utils/SystemClock.h>
 
+#include <array>
+#include <set>
+
 #include "SurroundView3dSession.h"
 #include "sv_3d_params.h"
 
@@ -37,6 +39,7 @@
 
 static const uint8_t kGrayColor = 128;
 static const int kNumChannels = 4;
+static const int kFrameDelayInMilliseconds = 30;
 
 SurroundView3dSession::SurroundView3dSession() :
     mStreamState(STOPPED){
@@ -285,15 +288,18 @@
 }
 
 void SurroundView3dSession::generateFrames() {
+    if (mSurroundView->Start3dPipeline()) {
+        LOG(INFO) << "Start3dPipeline succeeded";
+    } else {
+        LOG(ERROR) << "Start3dPipeline failed";
+        return;
+    }
+
     int sequenceId = 0;
 
     // TODO(b/150412555): do not use the setViews for frames generation
     // since there is a discrepancy between the HIDL APIs and core lib APIs.
-    vector<vector<float>> matrix;
-    matrix.resize(4);
-    for (int i=0; i<4; i++) {
-        matrix[i].resize(4);
-    }
+    array<array<float, 4>, 4> matrix;
 
     while(true) {
         {
@@ -349,9 +355,9 @@
             }
         }
 
-        // TODO(b/150412555): use hard-coded views for now. Change view every 10
-        // frames.
-        int recViewId = sequenceId / 10 % 16;
+        // TODO(b/150412555): use hard-coded views for now. Change view every
+        // frame.
+        int recViewId = sequenceId % 16;
         for (int i=0; i<4; i++)
             for (int j=0; j<4; j++) {
                 matrix[i][j] = kRecViews[recViewId][i*4+j];
@@ -426,6 +432,11 @@
                 mStream->receiveFrames(framesRecord.frames);
             }
         }
+
+        // TODO(b/150412555): adding delays explicitly. This delay should be
+        // removed when EVS camera is used.
+        this_thread::sleep_for(chrono::milliseconds(
+            kFrameDelayInMilliseconds));
     }
 
     // If we've been asked to stop, send an event to signal the actual end of stream
@@ -442,8 +453,15 @@
     // description.
     mSurroundView = unique_ptr<SurroundView>(Create());
 
-    mSurroundView->SetStaticData(GetCameras(), Get2dParams(), Get3dParams(),
-                                 GetUndistortionScales(), GetBoundingBox());
+    SurroundViewStaticDataParams params =
+        SurroundViewStaticDataParams(GetCameras(),
+                                     Get2dParams(),
+                                     Get3dParams(),
+                                     GetUndistortionScales(),
+                                     GetBoundingBox(),
+                                     map<string, CarTexture>(),
+                                     map<string, CarPart>());
+    mSurroundView->SetStaticData(params);
 
     // TODO(b/150412555): remove after EVS camera is used
     mInputPointers = mSurroundView->ReadImages(
@@ -491,13 +509,6 @@
         return false;
     }
 
-    if (mSurroundView->Start3dPipeline()) {
-        LOG(INFO) << "Start3dPipeline succeeded";
-    } else {
-        LOG(ERROR) << "Start3dPipeline failed";
-        return false;
-    }
-
     mIsInitialized = true;
     return true;
 }
diff --git a/surround_view/service-impl/core_lib.h b/surround_view/service-impl/core_lib.h
index 04ff43a..c2d31af 100644
--- a/surround_view/service-impl/core_lib.h
+++ b/surround_view/service-impl/core_lib.h
@@ -1,7 +1,10 @@
 #ifndef WIRELESS_ANDROID_AUTOMOTIVE_CAML_SURROUND_VIEW_CORE_LIB_H_
 #define WIRELESS_ANDROID_AUTOMOTIVE_CAML_SURROUND_VIEW_CORE_LIB_H_
 
+#include <array>
 #include <cstdint>
+#include <map>
+#include <string>
 #include <vector>
 
 namespace android_auto {
@@ -341,25 +344,14 @@
   // RGBA values, A is used for transparency.
   uint8_t rgba[4];
 
-  // normalized texture coordinates, in width and height direction. Range [0,
-  // 1].
-  float tex[2];
-
-  // normalized vertex normal.
-  float nor[3];
-
   bool operator==(const OverlayVertex& rhs) const {
     return (0 == std::memcmp(pos, rhs.pos, 3 * sizeof(float))) &&
-           (0 == std::memcmp(rgba, rhs.rgba, 4 * sizeof(uint8_t))) &&
-           (0 == std::memcmp(tex, rhs.tex, 2 * sizeof(float))) &&
-           (0 == std::memcmp(nor, rhs.nor, 3 * sizeof(float)));
+           (0 == std::memcmp(rgba, rhs.rgba, 4 * sizeof(uint8_t)));
   }
 
   OverlayVertex& operator=(const OverlayVertex& rhs) {
     std::memcpy(pos, rhs.pos, 3 * sizeof(float));
     std::memcpy(rgba, rhs.rgba, 4 * sizeof(uint8_t));
-    std::memcpy(tex, rhs.tex, 2 * sizeof(float));
-    std::memcpy(nor, rhs.nor, 3 * sizeof(float));
     return *this;
   }
 };
@@ -386,12 +378,253 @@
   }
 };
 
+// -----------   Structs related to car model  ---------------
+
+// 3D Vertex of a car model with normal and optionally texture coordinates.
+struct CarVertex {
+  // 3d position in (x, y, z).
+  std::array<float, 3> pos;
+
+  // unit normal at vertex, used for diffuse shading.
+  std::array<float, 3> normal;
+
+  // texture coordinates, valid in range [0, 1]. (-1, -1) implies no
+  // texture sampling. Note: only a single texture coordinate is currently
+  // supported per vertex. This struct will need to be extended with another
+  // tex_coord if multiple textures are needed per vertex.
+  std::array<float, 2> tex_coord;
+
+  // Default constructor.
+  CarVertex() {
+    pos = {0, 0, 0};
+    normal = {1, 0, 0};
+    tex_coord = {-1.0f, -1.0f};
+  }
+
+  CarVertex(const std::array<float, 3>& _pos,
+            const std::array<float, 3>& _normal,
+            const std::array<float, 2> _tex_coord)
+      : pos(_pos), normal(_normal), tex_coord(_tex_coord) {}
+};
+
+// Type of texture (color, bump, procedural etc.)
+// Currently only color is supported.
+enum CarTextureType : uint32_t {
+  // Texture map is applied to all color parameters: Ka, Kd and Ks.
+  // Data type of texture is RGB with each channel a uint8_t.
+  kKa = 0,
+  kKd,
+  kKs,
+
+  // Texture for bump maps. Data type is 3 channel float.
+  kBumpMap
+};
+
+// Textures to be used for rendering car model.
+struct CarTexture {
+  // Type and number of channels are dependant on each car texture type.
+  int width;
+  int height;
+  int channels;
+  int bytes_per_channel;
+  uint8_t* data;
+
+  CarTexture() {
+    width = 0;
+    height = 0;
+    channels = 0;
+    bytes_per_channel = 0;
+    data = nullptr;
+  }
+};
+
+// Material parameters for a car part.
+// Refer to MTL properties: http://paulbourke.net/dataformats/mtl/
+struct CarMaterial {
+  // Illumination model - 0, 1, 2 currently supported
+  // 0 = Color on and Ambient off
+  // 1 = Color on and Ambient on
+  // 2 = Highlight on
+  // 3 = Reflection on and Ray trace on
+  // 4 - 10 = Reflection/Transparency options not supported,
+  //          Will default to option 3.
+  uint8_t illum;
+
+  std::array<float, 3> ka;  // Ambient RGB [0, 1]
+  std::array<float, 3> kd;  // Diffuse RGB [0, 1]
+  std::array<float, 3> ks;  // Specular RGB [0, 1]
+
+  // Dissolve factor [0, 1], 0 = full transparent, 1 = full opaque.
+  float d;
+
+  // Specular exponent typically range from 0 to 1000.
+  // A high exponent results in a tight, concentrated highlight.
+  float ns;
+
+  // Set default values of material.
+  CarMaterial() {
+    illum = 0;                // Color on, ambient off
+    ka = {0.0f, 0.0f, 0.0f};  // No ambient.
+    kd = {0.0f, 0.0f, 0.0f};  // No dissolve.
+    ks = {0.0f, 0.0f, 0.0f};  // No specular.
+    d = 1.0f;                 // Fully opaque.
+    ns = 0;                   // No specular exponent.
+  }
+
+  // Map for texture type to a string id of a texture.
+  std::map<CarTextureType, std::string> textures;
+};
+
+// Type alias for 4x4 homogenous matrix, in row-major order.
+using Mat4x4 = std::array<float, 16>;
+
+// Represents a part of a car model.
+// Each car part is a object in the car that is individually animated and
+// has the same illumination properties. A car part may contain sub parts.
+struct CarPart {
+  // Car part vertices.
+  std::vector<CarVertex> vertices;
+
+  // Properties/attributes describing car material.
+  CarMaterial material;
+
+  // Model matrix to transform the car part from object space to its parent's
+  // coordinate space.
+  // The car's vertices are transformed by performing:
+  // parent_model_mat * model_mat * car_part_vertices to transform them to the
+  // global coordinate space.
+  // Model matrix must be a homogenous matrix with orthogonal rotation matrix.
+  Mat4x4 model_mat;
+
+  // Id of parent part. Parent part's model matrix is used to animate this part.
+  // empty string implies the part has no parent.
+  std::string parent_part_id;
+
+  // Ids of child parts. If current part is animated all its child parts
+  // are animated as well. Empty vector implies part has not children.
+  std::vector<std::string> child_part_ids;
+
+  CarPart(const std::vector<CarVertex>& car_vertices,
+          const CarMaterial& car_material, const Mat4x4& car_model_mat,
+          std::string car_parent_part_id,
+          const std::vector<std::string>& car_child_part_ids)
+      : vertices(car_vertices),
+        material(car_material),
+        model_mat(car_model_mat),
+        parent_part_id(car_parent_part_id),
+        child_part_ids(car_child_part_ids) {}
+
+  CarPart& operator=(const CarPart& car_part) {
+    this->vertices = car_part.vertices;
+    this->material = car_part.material;
+    this->model_mat = car_part.model_mat;
+    this->parent_part_id = car_part.parent_part_id;
+    this->child_part_ids = car_part.child_part_ids;
+    return *this;
+  }
+};
+
+struct AnimationParam {
+  // part id
+  std::string part_id;
+
+  // model matrix.
+  Mat4x4 model_matrix;
+
+  // bool flag indicating if the model matrix is updated from last
+  // SetAnimations() call.
+  bool is_model_update;
+
+  // gamma.
+  float gamma;
+
+  // bool flag indicating if gamma is updated from last
+  // SetAnimations() call.
+  bool is_gamma_update;
+
+  // texture id.
+  std::string texture_id;
+
+  // bool flag indicating if texture is updated from last
+  // SetAnimations() call.
+  bool is_texture_update;
+
+  // Default constructor, no animations are updated.
+  AnimationParam() {
+    is_model_update = false;
+    is_gamma_update = false;
+    is_texture_update = false;
+  }
+
+  // Constructor with car part name.
+  explicit AnimationParam(const std::string& _part_id)
+      : part_id(_part_id),
+        is_model_update(false),
+        is_gamma_update(false),
+        is_texture_update(false) {}
+
+  void SetModelMatrix(const Mat4x4& model_mat) {
+    is_model_update = true;
+    model_matrix = model_mat;
+  }
+
+  void SetGamma(float gamma_value) {
+    is_gamma_update = true;
+    gamma = gamma_value;
+  }
+
+  void SetTexture(const std::string& tex_id) {
+    is_texture_update = true;
+    texture_id = tex_id;
+  }
+};
+
 enum Format {
   GRAY = 0,
   RGB = 1,
   RGBA = 2,
 };
 
+// collection of surround view static data params.
+struct SurroundViewStaticDataParams {
+  std::vector<SurroundViewCameraParams> cameras_params;
+
+  // surround view 2d parameters.
+  SurroundView2dParams surround_view_2d_params;
+
+  // surround view 3d parameters.
+  SurroundView3dParams surround_view_3d_params;
+
+  // undistortion focal length scales.
+  std::vector<float> undistortion_focal_length_scales;
+
+  // car model bounding box for 2d surround view.
+  BoundingBox car_model_bb;
+
+  // map of texture name to a car texture. Lists all textures to be
+  // used for car model rendering.
+  std::map<std::string, CarTexture> car_textures;
+
+  // map of car id to a car part. Lists all car parts to be used
+  // for car model rendering.
+  std::map<std::string, CarPart> car_parts;
+
+  SurroundViewStaticDataParams(
+      const std::vector<SurroundViewCameraParams>& sv_cameras_params,
+      const SurroundView2dParams& sv_2d_params,
+      const SurroundView3dParams& sv_3d_params,
+      const std::vector<float>& scales, const BoundingBox& bb,
+      const std::map<std::string, CarTexture>& textures,
+      const std::map<std::string, CarPart>& parts)
+      : cameras_params(sv_cameras_params),
+        surround_view_2d_params(sv_2d_params),
+        surround_view_3d_params(sv_3d_params),
+        undistortion_focal_length_scales(scales),
+        car_model_bb(bb),
+        car_textures(textures),
+        car_parts(parts) {}
+};
+
 struct SurroundViewInputBufferPointers {
   void* gpu_data_pointer;
   void* cpu_data_pointer;
@@ -418,17 +651,41 @@
   Format format;
   int width;
   int height;
-  SurroundViewResultPointer() : data_pointer(nullptr), width(0), height(0) {}
+  bool is_data_preallocated;
+  SurroundViewResultPointer()
+      : data_pointer(nullptr),
+        width(0),
+        height(0),
+        is_data_preallocated(false) {}
+
+  // Constructor with result data pointer being allocated within core lib.
+  // Use for cases when no already existing buffer is available.
   SurroundViewResultPointer(Format format_, int width_, int height_)
       : format(format_), width(width_), height(height_) {
     // default formate is gray.
     const int byte_per_pixel = format_ == RGB ? 3 : format_ == RGBA ? 4 : 1;
     data_pointer =
         static_cast<void*>(new char[width * height * byte_per_pixel]);
+    is_data_preallocated = false;
   }
+
+  // Constructor with pre-allocated data.
+  // Use for cases when results must be added to an existing allocated buffer.
+  // Example, pre-allocated buffer of a display.
+  SurroundViewResultPointer(void* data_pointer_, Format format_, int width_,
+                            int height_)
+      : data_pointer(data_pointer_),
+        format(format_),
+        width(width_),
+        height(height_),
+        is_data_preallocated(true) {}
+
   ~SurroundViewResultPointer() {
     if (data_pointer) {
-      // delete[] static_cast<char*>(data_pointer);
+      // TODO(b/154365307): Fix freeing up of pre-allocated memory.
+      // if (!is_data_preallocated) {
+      //   delete[] static_cast<char*>(data_pointer);
+      // }
       data_pointer = nullptr;
     }
   }
@@ -439,13 +696,10 @@
   virtual ~SurroundView() = default;
 
   // Sets SurroundView static data.
-  // For each input, please refer to the definition.
+  // For details of SurroundViewStaticDataParams, please refer to the
+  // definition.
   virtual bool SetStaticData(
-      const std::vector<SurroundViewCameraParams>& cameras_params,
-      const SurroundView2dParams& surround_view_2d_params,
-      const SurroundView3dParams& surround_view_3d_params,
-      const std::vector<float>& undistortion_focal_length_scales,
-      const BoundingBox& car_model_bb) = 0;
+      const SurroundViewStaticDataParams& static_data_params) = 0;
 
   // Starts 2d pipeline. Returns false if error occurs.
   virtual bool Start2dPipeline() = 0;
@@ -461,29 +715,29 @@
   virtual void Stop3dPipeline() = 0;
 
   // Updates 2d output resolution on-the-fly. Starts2dPipeline() must be called
-  // before this can be called. For quality assurance, the resolution should not
-  // be larger than the original one. This call is not thread safe and there is
-  // no sync between Get2dSurroundView() and this call.
+  // before this can be called. For quality assurance, the |resolution| should
+  // not be larger than the original one. This call is not thread safe and there
+  // is no sync between Get2dSurroundView() and this call.
   virtual bool Update2dOutputResolution(const Size2dInteger& resolution) = 0;
 
   // Updates 3d output resolution on-the-fly. Starts3dPipeline() must be called
-  // before this can be called. For quality assurance, the resolution should not
-  // be larger than the original one. This call is not thread safe and there is
-  // no sync between Get3dSurroundView() and this call.
+  // before this can be called. For quality assurance, the |resolution| should
+  // not be larger than the original one. This call is not thread safe and there
+  // is no sync between Get3dSurroundView() and this call.
   virtual bool Update3dOutputResolution(const Size2dInteger& resolution) = 0;
 
   // Projects camera's pixel location to surround view 2d image location.
-  // camera_point is the pixel location in raw camera's space.
-  // camera_index is the camera's index.
-  // surround_view_2d_point is the surround view 2d image pixel location.
+  // |camera_point| is the pixel location in raw camera's space.
+  // |camera_index| is the camera's index.
+  // |surround_view_2d_point| is the surround view 2d image pixel location.
   virtual bool GetProjectionPointFromRawCameraToSurroundView2d(
       const Coordinate2dInteger& camera_point, int camera_index,
       Coordinate2dFloat* surround_view_2d_point) = 0;
 
   // Projects camera's pixel location to surround view 3d bowl coordinate.
-  // camera_point is the pixel location in raw camera's space.
-  // camera_index is the camera's index.
-  // surround_view_3d_point is the surround view 3d vertex.
+  // |camera_point| is the pixel location in raw camera's space.
+  // |camera_index| is the camera's index.
+  // |surround_view_3d_point| is the surround view 3d vertex.
   virtual bool GetProjectionPointFromRawCameraToSurroundView3d(
       const Coordinate2dInteger& camera_point, int camera_index,
       Coordinate3dFloat* surround_view_3d_point) = 0;
@@ -497,19 +751,41 @@
       SurroundViewResultPointer* result_pointer) = 0;
 
   // Gets 3d surround view image.
-  // It takes input_pointers and view_matrix as input, and output is
-  // result_pointer. view_matrix is 4 x 4 matrix.
+  // It takes |input_pointers| and |view_matrix| as input, and output is
+  // |result_pointer|. |view_matrix| is 4 x 4 matrix.
   // Please refer to the definition of
   // SurroundViewInputBufferPointers and
   // SurroundViewResultPointer.
   virtual bool Get3dSurroundView(
       const std::vector<SurroundViewInputBufferPointers>& input_pointers,
-      const std::vector<std::vector<float>> view_matrix,
+      const std::array<std::array<float, 4>, 4>& view_matrix,
+      SurroundViewResultPointer* result_pointer) = 0;
+
+  // Gets 3d surround view image overload.
+  // It takes |input_pointers|, |quaternion| and |translation| as input,
+  // and output is |result_pointer|.
+  // |quaternion| is 4 x 1 array (X, Y, Z, W).
+  // It is required to be unit quaternion as rotation quaternion.
+  // |translation| is 3 X 1 array (x, y, z).
+  // Please refer to the definition of
+  // SurroundViewInputBufferPointers and
+  // SurroundViewResultPointer.
+  virtual bool Get3dSurroundView(
+      const std::vector<SurroundViewInputBufferPointers>& input_pointers,
+      const std::array<float, 4>& quaternion,
+      const std::array<float, 3>& translation,
       SurroundViewResultPointer* result_pointer) = 0;
 
   // Sets 3d overlays.
   virtual bool Set3dOverlay(const std::vector<Overlay>& overlays) = 0;
 
+  // Animates a set of car parts.
+  // Only updated car parts are included.
+  // |car_animations| is a vector of AnimationParam specifying updated
+  // car parts with updated animation parameters.
+  virtual bool SetAnimations(
+      const std::vector<AnimationParam>& car_animations) = 0;
+
   // for test only.
   // TODO(xxqian): remove thest two fns.
   virtual std::vector<SurroundViewInputBufferPointers> ReadImages(
diff --git a/surround_view/service-impl/lib/arm64/libcore_lib_shared.so b/surround_view/service-impl/lib/arm64/libcore_lib_shared.so
old mode 100644
new mode 100755
index 0175c16..2421d41
--- a/surround_view/service-impl/lib/arm64/libcore_lib_shared.so
+++ b/surround_view/service-impl/lib/arm64/libcore_lib_shared.so
Binary files differ
diff --git a/surround_view/service-impl/lib/x86-64/libcore_lib.so b/surround_view/service-impl/lib/x86-64/libcore_lib.so
deleted file mode 100755
index 96479c6..0000000
--- a/surround_view/service-impl/lib/x86-64/libcore_lib.so
+++ /dev/null
Binary files differ
diff --git a/surround_view/service-impl/lib/x86-64/libcore_lib_shared.so b/surround_view/service-impl/lib/x86-64/libcore_lib_shared.so
new file mode 100755
index 0000000..34d1f6a
--- /dev/null
+++ b/surround_view/service-impl/lib/x86-64/libcore_lib_shared.so
Binary files differ
diff --git a/surround_view/service-impl/lib/x86/libcore_lib.so b/surround_view/service-impl/lib/x86/libcore_lib.so
deleted file mode 100755
index 34e3bcb..0000000
--- a/surround_view/service-impl/lib/x86/libcore_lib.so
+++ /dev/null
Binary files differ
diff --git a/surround_view/service-impl/lib/x86/libcore_lib_shared.so b/surround_view/service-impl/lib/x86/libcore_lib_shared.so
new file mode 100755
index 0000000..c6ba2b6
--- /dev/null
+++ b/surround_view/service-impl/lib/x86/libcore_lib_shared.so
Binary files differ
diff --git a/tests/CarSecurityPermissionTest/Android.bp b/tests/CarSecurityPermissionTest/Android.bp
index 330568d..066ecc8 100644
--- a/tests/CarSecurityPermissionTest/Android.bp
+++ b/tests/CarSecurityPermissionTest/Android.bp
@@ -31,6 +31,7 @@
         "androidx.test.ext.junit",
         "androidx.test.rules",
         "car-frameworks-service",
+        "compatibility-device-util-axt",
         "mockito-target-minus-junit4",
         "testng",
         "truth-prebuilt",
diff --git a/tests/CarSecurityPermissionTest/src/com/android/car/user/CarUserManagerPermissionTest.java b/tests/CarSecurityPermissionTest/src/com/android/car/user/CarUserManagerPermissionTest.java
new file mode 100644
index 0000000..86a2853
--- /dev/null
+++ b/tests/CarSecurityPermissionTest/src/com/android/car/user/CarUserManagerPermissionTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+
+package com.android.car.user;
+
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.MANAGE_USERS;
+import static android.car.Car.CAR_USER_SERVICE;
+import static android.car.Car.createCar;
+import static android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationType.CUSTOM_1;
+
+import static com.android.compatibility.common.util.ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.expectThrows;
+
+import android.app.Instrumentation;
+import android.car.Car;
+import android.car.user.CarUserManager;
+import android.car.user.CarUserManager.UserLifecycleListener;
+import android.content.Context;
+import android.os.Handler;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Objects;
+
+/**
+ * This class contains security permission tests for the {@link CarUserManager}'s system APIs.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class CarUserManagerPermissionTest {
+    private static final int USRE_TYPE = 1;
+
+    private CarUserManager mCarUserManager;
+    private Context mContext;
+    private Instrumentation mInstrumentation;
+
+    @Before
+    public void setUp() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getTargetContext();
+        Car car = Objects.requireNonNull(createCar(mContext, (Handler) null));
+        mCarUserManager = (CarUserManager) car.getCarManager(CAR_USER_SERVICE);
+
+    }
+
+    @Test
+    public void testSwitchUserPermission() throws Exception {
+        int target_uid = 100;
+        Exception e = expectThrows(SecurityException.class,
+                () -> mCarUserManager.switchUser(target_uid));
+        assertThat(e.getMessage()).contains(MANAGE_USERS);
+    }
+
+    @Test
+    public void testAddListenerPermission() {
+        UserLifecycleListener listener = (e) -> { };
+
+        Exception e = expectThrows(SecurityException.class,
+                () -> mCarUserManager.addListener(Runnable::run, listener));
+
+        assertThat(e.getMessage()).contains(INTERACT_ACROSS_USERS);
+        assertThat(e.getMessage()).contains(INTERACT_ACROSS_USERS_FULL);
+    }
+
+    @Test
+    public void testRemoveListenerPermission() throws Exception {
+        UserLifecycleListener listener = (e) -> { };
+        invokeMethodWithShellPermissionsNoReturn(mCarUserManager,
+                (um) -> um.addListener(Runnable::run, listener));
+
+        Exception e = expectThrows(SecurityException.class,
+                () -> mCarUserManager.removeListener(listener));
+
+        assertThat(e.getMessage()).contains(INTERACT_ACROSS_USERS);
+        assertThat(e.getMessage()).contains(INTERACT_ACROSS_USERS_FULL);
+    }
+
+    @Test
+    public void testGetUserIdentificationAssociationPermission() {
+        Exception e = expectThrows(SecurityException.class,
+                () -> mCarUserManager.getUserIdentificationAssociation(CUSTOM_1));
+        assertThat(e.getMessage()).contains(MANAGE_USERS);
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociationPermission() {
+        Exception e = expectThrows(SecurityException.class,
+                () -> mCarUserManager.setUserIdentificationAssociation(
+                        new int[] {CUSTOM_1}, new int[] {42}));
+        assertThat(e.getMessage()).contains(MANAGE_USERS);
+    }
+
+    @Test
+    public void testSetUserSwitchUiCallback() {
+        Exception e = expectThrows(SecurityException.class,
+                () -> mCarUserManager.getUserIdentificationAssociation(CUSTOM_1));
+        assertThat(e.getMessage()).contains(MANAGE_USERS);
+    }
+}
diff --git a/tests/SecondaryHomeApp/Android.bp b/tests/SecondaryHomeApp/Android.bp
index ce8e763..04da06b 100644
--- a/tests/SecondaryHomeApp/Android.bp
+++ b/tests/SecondaryHomeApp/Android.bp
@@ -22,14 +22,10 @@
     ],
 
     static_libs: [
-        "android.car.userlib",
         "androidx.appcompat_appcompat",
-        "androidx.recyclerview_recyclerview",
-        "androidx.legacy_legacy-support-v4",
         "androidx.lifecycle_lifecycle-extensions",
         "com.google.android.material_material",
         "CarNotificationLib",
-        "car-ui-lib"
     ],
 
     libs: [
diff --git a/tests/SecondaryHomeApp/AndroidManifest.xml b/tests/SecondaryHomeApp/AndroidManifest.xml
index 0a84531..5d22b70 100644
--- a/tests/SecondaryHomeApp/AndroidManifest.xml
+++ b/tests/SecondaryHomeApp/AndroidManifest.xml
@@ -45,6 +45,7 @@
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.SECONDARY_HOME"/>
             </intent-filter>
         </activity>
         <service android:name=".launcher.NotificationListener"
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarActivityViewDisplayIdTest.java b/tests/android_car_api_test/src/android/car/apitest/CarActivityViewDisplayIdTest.java
index 93f2530..ee6dbbb 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarActivityViewDisplayIdTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarActivityViewDisplayIdTest.java
@@ -24,7 +24,6 @@
 import static org.junit.Assume.assumeTrue;
 import static org.testng.Assert.assertThrows;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 
 import org.junit.Test;
@@ -42,7 +41,6 @@
     private static final int NONEXISTENT_DISPLAY_ID = Integer.MAX_VALUE;
 
     @Test
-    @FlakyTest
     public void testSingleActivityView() throws Exception {
         ActivityViewTestActivity activity = startActivityViewTestActivity(DEFAULT_DISPLAY);
         activity.waitForActivityViewReady();
@@ -65,7 +63,6 @@
     }
 
     @Test
-    @FlakyTest
     public void testDoubleActivityView() throws Exception {
         ActivityViewTestActivity activity1 = startActivityViewTestActivity(DEFAULT_DISPLAY);
         activity1.waitForActivityViewReady();
@@ -100,7 +97,6 @@
     }
 
     @Test
-    @FlakyTest
     public void testThrowsExceptionOnReportingNonExistingDisplay() throws Exception {
         ActivityViewTestActivity activity = startActivityViewTestActivity(DEFAULT_DISPLAY);
         activity.waitForActivityViewReady();
@@ -125,7 +121,6 @@
 
     // TODO(b/143353546): Make the following tests not to rely on CarLauncher.
     @Test
-    @FlakyTest
     public void testThrowsExceptionOnReportingNonOwningDisplay() throws Exception {
         int displayIdOfCarLauncher = waitForActivityViewDisplayReady(CAR_LAUNCHER_PKG_NAME);
         assumeTrue(INVALID_DISPLAY != displayIdOfCarLauncher);
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
new file mode 100644
index 0000000..42406ca
--- /dev/null
+++ b/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+package android.car.apitest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+import static org.testng.Assert.expectThrows;
+
+import android.Manifest;
+import android.annotation.FloatRange;
+import android.car.Car;
+import android.car.CarBugreportManager;
+import android.car.CarBugreportManager.CarBugreportManagerCallback;
+import android.os.ParcelFileDescriptor;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CarBugreportManagerTest extends CarApiTestBase {
+    // TODO: Use "dumpstate.dry_run" to make dumpstate faster.
+    // dumpstate runs around 3 minutes on emulator on a pretty fast computer.
+    private static final int BUGREPORT_TIMEOUT_MILLIS = 360_000;
+    private static final int NO_ERROR = -1;
+
+    @Rule public TestName testName = new TestName();
+
+    private CarBugreportManager mManager;
+    private FakeCarBugreportCallback mFakeCallback;
+    private ParcelFileDescriptor mOutput;
+    private ParcelFileDescriptor mExtraOutput;
+
+    @Before
+    public void setUp() throws Exception {
+        mManager = (CarBugreportManager) getCar().getCarManager(Car.CAR_BUGREPORT_SERVICE);
+        mFakeCallback = new FakeCarBugreportCallback();
+        mOutput = createParcelFdInCache("bugreport", "zip");
+        mExtraOutput = createParcelFdInCache("screenshot", "png");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        getPermissions();  // For cancelBugreport()
+        mManager.cancelBugreport();
+        dropPermissions();
+    }
+
+    @Test
+    public void test_requestBugreport_failsWhenNoPermission() throws Exception {
+        dropPermissions();
+
+        SecurityException expected =
+                expectThrows(SecurityException.class,
+                        () -> mManager.requestBugreport(mOutput, mExtraOutput, mFakeCallback));
+        assertThat(expected).hasMessageThat().contains(
+                "nor current process has android.permission.DUMP.");
+    }
+
+    @Test
+    @LargeTest
+    public void test_requestBugreport_works() throws Exception {
+        getPermissions();
+
+        mManager.requestBugreport(mOutput, mExtraOutput, mFakeCallback);
+
+        // The FDs are duped and closed in requestBugreport().
+        assertFdIsClosed(mOutput);
+        assertFdIsClosed(mExtraOutput);
+
+        mFakeCallback.waitTillDoneOrTimeout(BUGREPORT_TIMEOUT_MILLIS);
+        assertThat(mFakeCallback.isFinishedSuccessfully()).isEqualTo(true);
+        assertThat(mFakeCallback.getReceivedProgress()).isTrue();
+        // TODO: Check the contents of the zip file and the extra output.
+    }
+
+    @Test
+    public void test_requestBugreport_cannotRunMultipleBugreports() throws Exception {
+        getPermissions();
+        FakeCarBugreportCallback callback2 = new FakeCarBugreportCallback();
+        ParcelFileDescriptor output2 = createParcelFdInCache("bugreport2", "zip");
+        ParcelFileDescriptor extraOutput2 = createParcelFdInCache("screenshot2", "png");
+
+        // 1st bugreport.
+        mManager.requestBugreport(mOutput, mExtraOutput, mFakeCallback);
+
+        // 2nd bugreport.
+        mManager.requestBugreport(output2, extraOutput2, callback2);
+
+        callback2.waitTillDoneOrTimeout(BUGREPORT_TIMEOUT_MILLIS);
+        assertThat(callback2.getErrorCode()).isEqualTo(
+                CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
+    }
+
+    @Test
+    @LargeTest
+    public void test_cancelBugreport_works() throws Exception {
+        getPermissions();
+        FakeCarBugreportCallback callback2 = new FakeCarBugreportCallback();
+        ParcelFileDescriptor output2 = createParcelFdInCache("bugreport2", "zip");
+        ParcelFileDescriptor extraOutput2 = createParcelFdInCache("screenshot2", "png");
+
+        // 1st bugreport.
+        mManager.requestBugreport(mOutput, mExtraOutput, mFakeCallback);
+        mManager.cancelBugreport();
+
+        // Allow the system to finish the bugreport cancellation, 0.5 seconds is enough.
+        Thread.sleep(500);
+
+        // 2nd bugreport must work, because 1st bugreport was cancelled.
+        mManager.requestBugreport(output2, extraOutput2, callback2);
+
+        callback2.waitTillProgressOrTimeout(BUGREPORT_TIMEOUT_MILLIS);
+        assertThat(callback2.getErrorCode()).isEqualTo(NO_ERROR);
+        assertThat(callback2.getReceivedProgress()).isEqualTo(true);
+    }
+
+    private static void getPermissions() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.DUMP);
+    }
+
+    private static void dropPermissions() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    /** Creates a temp file in cache dir and returns file descriptor. */
+    private ParcelFileDescriptor createParcelFdInCache(String prefix, String extension)
+            throws Exception {
+        File f = File.createTempFile(
+                prefix + "_" + testName.getMethodName(), extension, getContext().getCacheDir());
+        f.setReadable(true, true);
+        f.setWritable(true, true);
+
+        return ParcelFileDescriptor.open(f,
+                ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
+    }
+
+    private static void assertFdIsClosed(ParcelFileDescriptor pfd) {
+        try {
+            int fd = pfd.getFd();
+            fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd);
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    private static class FakeCarBugreportCallback extends CarBugreportManagerCallback {
+        private final Object mLock = new Object();
+        private final CountDownLatch mEndedLatch = new CountDownLatch(1);
+        private final CountDownLatch mProgressLatch = new CountDownLatch(1);
+        private boolean mReceivedProgress = false;
+        private int mErrorCode = NO_ERROR;
+
+        @Override
+        public void onProgress(@FloatRange(from = 0f, to = 100f) float progress) {
+            synchronized (mLock) {
+                mReceivedProgress = true;
+            }
+            mProgressLatch.countDown();
+        }
+
+        @Override
+        public void onError(
+                @CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) {
+            synchronized (mLock) {
+                mErrorCode = errorCode;
+            }
+            mEndedLatch.countDown();
+            mProgressLatch.countDown();
+        }
+
+        @Override
+        public void onFinished() {
+            mEndedLatch.countDown();
+            mProgressLatch.countDown();
+        }
+
+        int getErrorCode() {
+            synchronized (mLock) {
+                return mErrorCode;
+            }
+        }
+
+        boolean getReceivedProgress() {
+            synchronized (mLock) {
+                return mReceivedProgress;
+            }
+        }
+
+        boolean isFinishedSuccessfully() {
+            return mEndedLatch.getCount() == 0 && getErrorCode() == NO_ERROR;
+        }
+
+        void waitTillDoneOrTimeout(long timeoutMillis) throws InterruptedException {
+            mEndedLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+        }
+
+        void waitTillProgressOrTimeout(long timeoutMillis) throws InterruptedException {
+            mProgressLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+        }
+    }
+}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarNavigationManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarNavigationManagerTest.java
index 0d92400..6732c3d 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarNavigationManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarNavigationManagerTest.java
@@ -43,7 +43,6 @@
 import com.google.android.collect.Lists;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 /**
@@ -112,7 +111,6 @@
         assertThat(NavigationStateProto.parseFrom(navigationStateProto.toByteArray())).isNotNull();
     }
 
-    @Ignore("TODO(b/15534360)")
     @Test
     public void testSendEvent() throws Exception {
         if (mCarNavigationManager == null) {
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 e18a200..ab13c5f 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
@@ -58,14 +58,14 @@
 
     private static final String TAG = CarUserManagerTest.class.getSimpleName();
 
-    private static final int SWITCH_TIMEOUT_MS = 40_000;
+    private static final int SWITCH_TIMEOUT_MS = 70_000;
     private static final int STOP_TIMEOUT_MS = 300_000;
 
     /**
      * Stopping the user takes a while, even when calling force stop - change it to false if this
      * test becomes flaky.
      */
-    private static final boolean TEST_STOP = true;
+    private static final boolean TEST_STOP = false;
 
     private static final UserManager sUserManager = UserManager.get(sContext);
 
@@ -75,7 +75,7 @@
     private CarUserManager mCarUserManager;
 
     @BeforeClass
-    public static void createUserFixture() {
+    public static void setupUsers() {
         sInitialUserId = ActivityManager.getCurrentUser();
         Log.i(TAG, "Running test as user " + sInitialUserId);
 
@@ -83,7 +83,9 @@
     }
 
     @AfterClass
-    public static void removeUserFixture() {
+    public static void cleanupUsers() {
+        switchUserDirectly(sInitialUserId);
+
         if (sNewUserId == UserHandle.USER_NULL) {
             Log.w(TAG, "No need to remove user" + sNewUserId);
             return;
@@ -108,7 +110,8 @@
         int oldUserId = sInitialUserId;
         int newUserId = sNewUserId;
 
-        BlockingUserLifecycleListener startListener = new BlockingUserLifecycleListener.Builder()
+        BlockingUserLifecycleListener startListener = BlockingUserLifecycleListener
+                .forSpecificEvents()
                 .forUser(newUserId)
                 .setTimeout(SWITCH_TIMEOUT_MS)
                 .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
@@ -152,7 +155,8 @@
         Log.d(TAG, "unregistering start listener: " + startListener);
         mCarUserManager.removeListener(startListener);
 
-        BlockingUserLifecycleListener stopListener = new BlockingUserLifecycleListener.Builder()
+        BlockingUserLifecycleListener stopListener = BlockingUserLifecycleListener
+                .forSpecificEvents()
                 .forUser(newUserId)
                 .setTimeout(STOP_TIMEOUT_MS)
                 .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPING)
@@ -165,16 +169,13 @@
         // Switch back to the previous user
         switchUser(oldUserId);
 
-        // Must force stop the user, otherwise it can take minutes for its process to finish
-        forceStopUser(newUserId);
-
         if (TEST_STOP) {
-            // waitForEvents() will also return events for previous user...
-            List<UserLifecycleEvent> allEvents = stopListener.waitForEvents();
-            Log.d(TAG, "All received events on stopListener: " + allEvents);
-            //... so we need to check for just the epected events
-            List<UserLifecycleEvent> stopEvents = stopListener.getExpectedEventsReceived();
-            Log.d(TAG, "Relevant stop events: " + stopEvents);
+            // Must force stop the user, otherwise it can take minutes for its process to finish
+            forceStopUser(newUserId);
+
+            List<UserLifecycleEvent> stopEvents = stopListener.waitForEvents();
+            Log.d(TAG, "stopEvents: " + stopEvents + "; all events on stop listener: "
+                    + stopListener.getAllReceivedEvents());
 
             // Assert user ids
             for (UserLifecycleEvent event : stopEvents) {
@@ -183,13 +184,15 @@
                 assertWithMessage("wrong userHandle on %s", event)
                     .that(event.getUserHandle().getIdentifier()).isEqualTo(newUserId);
             }
+        } else {
+            Log.w(TAG, "NOT testing user stop events");
         }
 
         // Make sure unregistered listener din't receive any more events
 
         List<UserLifecycleEvent> allStartEvents = startListener.getAllReceivedEvents();
         Log.d(TAG, "All start events: " + startEvents);
-        assertThat(allStartEvents).isSameAs(startEvents);
+        assertThat(allStartEvents).containsAllIn(startEvents).inOrder();
 
         Log.d(TAG, "unregistering stop listener: " + stopListener);
         mCarUserManager.removeListener(stopListener);
diff --git a/tests/android_car_api_test/src/android/car/apitest/PreInstalledPackagesTest.java b/tests/android_car_api_test/src/android/car/apitest/PreInstalledPackagesTest.java
new file mode 100644
index 0000000..5693800
--- /dev/null
+++ b/tests/android_car_api_test/src/android/car/apitest/PreInstalledPackagesTest.java
@@ -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.
+ */
+package android.car.apitest;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import static org.junit.Assert.fail;
+
+import android.text.TextUtils;
+
+import org.junit.Test;
+
+public final class PreInstalledPackagesTest {
+
+    @Test
+    public void testNoCriticalErrors_currentMode() {
+        assertNoCriticalErrors(/* enforceMode= */ false);
+    }
+
+    @Test
+    public void testNoCriticalErrors_enforceMode() {
+        assertNoCriticalErrors(/* enforceMode= */ true);
+    }
+
+    private static void assertNoCriticalErrors(boolean enforceMode) {
+        String cmd = "cmd user report-system-user-package-whitelist-problems --critical-only%s";
+        String mode =  enforceMode ? " --mode 1" : "";
+        String result = runShellCommand(cmd, mode);
+        if (!TextUtils.isEmpty(result)) {
+            fail("Command '" + cmd + " reported errors:\n" + result);
+        }
+    }
+}
diff --git a/tests/carservice_test/src/com/android/car/audio/CarAudioFocusTest.java b/tests/carservice_test/src/com/android/car/audio/CarAudioFocusTest.java
index fcbaefe..f855f2d 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarAudioFocusTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarAudioFocusTest.java
@@ -26,6 +26,7 @@
 import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.os.Looper;
+import android.util.Log;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.FlakyTest;
@@ -34,15 +35,22 @@
 
 import com.android.car.R;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 public class CarAudioFocusTest {
+
+    private static final String TAG = CarAudioFocusTest.class.getSimpleName();
+    private static final boolean DEBUG = false;
     private static final long TEST_TIMING_TOLERANCE_MS = 100;
     private static final int TEST_TORELANCE_MAX_ITERATIONS = 5;
     private static final int INTERACTION_REJECT = 0;  // Focus not granted
@@ -115,6 +123,8 @@
             .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
             .build();
 
+    private final Set<AudioFocusRequest> mAudioFocusRequestsSet = new HashSet<>();
+
     private AudioManager mAudioManager;
 
     @Before
@@ -128,8 +138,21 @@
                 isDynamicRoutingEnabled);
     }
 
+    @After
+    public void cleanUp() {
+        Iterator<AudioFocusRequest> iterator = mAudioFocusRequestsSet.iterator();
+        while (iterator.hasNext()) {
+            AudioFocusRequest request = iterator.next();
+            mAudioManager.abandonAudioFocusRequest(request);
+            if (DEBUG) {
+                Log.d(TAG, "cleanUp Removing: "
+                        + request.getAudioAttributes().usageToString());
+            }
+        }
+    }
+
     @Test
-    public void individualAttributeFocusRequest_focusRequestGranted() throws Exception {
+    public void individualAttributeFocusRequest_focusRequestGranted() {
         // Make sure each usage is able to request and release audio focus individually
         requestAndLoseFocusForAttribute(ATTR_INVALID);
         requestAndLoseFocusForAttribute(ATTR_MEDIA);
@@ -148,8 +171,7 @@
 
     @Test
     @FlakyTest
-    public void exclusiveInteractionsForFocusGain_requestGrantedAndFocusLossSent()
-            throws Exception {
+    public void exclusiveInteractionsForFocusGain_requestGrantedAndFocusLossSent() {
         // For each interaction the focus request is granted and on the second request
         // focus lost is dispatched to the first focus listener
 
@@ -161,8 +183,7 @@
     }
 
     @Test
-    public void exclusiveInteractionsTransient_requestGrantedAndFocusLossSent()
-            throws Exception {
+    public void exclusiveInteractionsTransient_requestGrantedAndFocusLossSent() {
         // For each interaction the focus request is granted and on the second request
         // focus lost transient is dispatched to the first focus listener
 
@@ -176,8 +197,7 @@
 
     @RequiresDevice
     @Test
-    public void exclusiveInteractionsTransientMayDuck_requestGrantedAndFocusLossSent()
-            throws Exception {
+    public void exclusiveInteractionsTransientMayDuck_requestGrantedAndFocusLossSent() {
         // For each interaction the focus request is granted and on the second request
         // focus lost transient is dispatched to the first focus listener
 
@@ -191,7 +211,7 @@
 
     @RequiresDevice
     @Test
-    public void rejectedInteractions_focusRequestRejected() throws Exception {
+    public void rejectedInteractions_focusRequestRejected() {
         // Test different paired interaction between different usages
         // for each interaction pair the first focus request will be granted but the second
         // will be rejected
@@ -257,7 +277,7 @@
     }
 
     @Test
-    public void concurrentInteractionsFocusGain_requestGrantedAndFocusLossSent() throws Exception {
+    public void concurrentInteractionsFocusGain_requestGrantedAndFocusLossSent() {
         // Test concurrent interactions i.e. interactions that can
         // potentially gain focus at the same time.
         // For this test permanent focus gain is requested by two usages.
@@ -283,8 +303,7 @@
 
     @RequiresDevice
     @Test
-    public void concurrentInteractionsTransientGainMayDuck_requestGrantedAndNoFocusLossSent()
-            throws Exception {
+    public void concurrentInteractionsTransientGainMayDuck_requestGrantedAndNoFocusLossSent() {
         // Test concurrent interactions i.e. interactions that can
         // potentially gain focus at the same time.
         // For this test permanent focus gain is requested by first usage and focus gain transient
@@ -296,8 +315,7 @@
         testConcurrentInteractions(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, true);
     }
 
-    private void testConcurrentInteractions(int gain, boolean pauseForDucking)
-            throws Exception {
+    private void testConcurrentInteractions(int gain, boolean pauseForDucking) {
         // Test paired concurrent interactions i.e. interactions that can
         // potentially gain focus at the same time.
         int interaction = INTERACTION_CONCURRENT;
@@ -400,8 +418,7 @@
         testInteraction(ATTR_ANNOUNCEMENT, ATTR_VEHICLE_STATUS, interaction, gain, pauseForDucking);
     }
 
-    private void testExclusiveInteractions(int gain, boolean pauseForDucking)
-            throws Exception {
+    private void testExclusiveInteractions(int gain, boolean pauseForDucking) {
 
         // Test exclusive interaction, interaction where each usage will not share focus with other
         // another usage. As a result once focus is gained any current focus listener
@@ -476,132 +493,125 @@
             AudioAttributes attributes2,
             int interaction,
             int gainType,
-            boolean pauseForDucking) throws Exception {
-        final AudioFocusRequest[] focusRequests = new AudioFocusRequest[2];
-        final FocusChangeListener[] focusListeners = new FocusChangeListener[2];
-        try {
-            final FocusChangeListener focusChangeListener1 = new FocusChangeListener();
-            final AudioFocusRequest audioFocusRequest1 = new AudioFocusRequest
-                    .Builder(AudioManager.AUDIOFOCUS_GAIN)
-                    .setAudioAttributes(attributes1)
-                    .setOnAudioFocusChangeListener(focusChangeListener1)
-                    .setForceDucking(false)
-                    .setWillPauseWhenDucked(pauseForDucking)
-                    .build();
-            focusRequests[0] = audioFocusRequest1;
-            focusListeners[0] = focusChangeListener1;
-            final FocusChangeListener focusChangeListener2 = new FocusChangeListener();
-            final AudioFocusRequest audioFocusRequest2 = new AudioFocusRequest
-                    .Builder(gainType)
-                    .setAudioAttributes(attributes2)
-                    .setOnAudioFocusChangeListener(focusChangeListener2)
-                    .setForceDucking(false)
-                    .build();
-            focusRequests[1] = audioFocusRequest2;
-            focusListeners[1] = focusChangeListener2;
+            boolean pauseForDucking) {
+        final FocusChangeListener focusChangeListener1 = new FocusChangeListener();
+        final AudioFocusRequest audioFocusRequest1 = new AudioFocusRequest
+                .Builder(AudioManager.AUDIOFOCUS_GAIN)
+                .setAudioAttributes(attributes1)
+                .setOnAudioFocusChangeListener(focusChangeListener1)
+                .setForceDucking(false)
+                .setWillPauseWhenDucked(pauseForDucking)
+                .build();
 
-            int expectedLoss = 0;
+        final FocusChangeListener focusChangeListener2 = new FocusChangeListener();
+        final AudioFocusRequest audioFocusRequest2 = new AudioFocusRequest
+                .Builder(gainType)
+                .setAudioAttributes(attributes2)
+                .setOnAudioFocusChangeListener(focusChangeListener2)
+                .setForceDucking(false)
+                .build();
 
-            // Each focus gain type will return a different focus lost type
-            switch (gainType) {
-                case AudioManager.AUDIOFOCUS_GAIN:
-                    expectedLoss = AudioManager.AUDIOFOCUS_LOSS;
-                    break;
-                case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
-                    expectedLoss = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
-                    break;
-                case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
-                    expectedLoss = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
-                    // Note loss or gain will not be sent as both can live concurrently
-                    if (interaction == INTERACTION_CONCURRENT && !pauseForDucking) {
-                        expectedLoss = AudioManager.AUDIOFOCUS_NONE;
-                    }
-                    break;
-            }
+        int expectedLoss = 0;
 
-            int secondRequestResultsExpected = AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+        // Each focus gain type will return a different focus lost type
+        switch (gainType) {
+            case AudioManager.AUDIOFOCUS_GAIN:
+                expectedLoss = AudioManager.AUDIOFOCUS_LOSS;
+                break;
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
+                expectedLoss = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
+                break;
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
+                expectedLoss = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT;
+                // Note loss or gain will not be sent as both can live concurrently
+                if (interaction == INTERACTION_CONCURRENT && !pauseForDucking) {
+                    expectedLoss = AudioManager.AUDIOFOCUS_NONE;
+                }
+                break;
+        }
 
-            if (interaction == INTERACTION_REJECT) {
-                secondRequestResultsExpected = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
-            }
+        int secondRequestResultsExpected = AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
 
-            int requestResult = mAudioManager.requestAudioFocus(audioFocusRequest1);
-            String message = "Focus gain request failed  for 1st "
+        if (interaction == INTERACTION_REJECT) {
+            secondRequestResultsExpected = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        }
+
+        int requestResult = mAudioManager.requestAudioFocus(audioFocusRequest1);
+        String message = "Focus gain request failed  for 1st "
+                + AudioAttributes.usageToString(attributes1.getSystemUsage());
+        assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
+        mAudioFocusRequestsSet.add(audioFocusRequest1);
+
+        requestResult = mAudioManager.requestAudioFocus(audioFocusRequest2);
+        message = "Focus gain request failed for 2nd "
+                + AudioAttributes.usageToString(attributes2.getSystemUsage());
+        assertEquals(message, secondRequestResultsExpected, requestResult);
+        mAudioFocusRequestsSet.add(audioFocusRequest2);
+
+        // If the results is rejected for second one we only have to clean up first
+        // as the second focus request is rejected
+        if (interaction == INTERACTION_REJECT) {
+            requestResult = mAudioManager.abandonAudioFocusRequest(audioFocusRequest1);
+            mAudioFocusRequestsSet.clear();
+            message = "Focus loss request failed for 1st "
                     + AudioAttributes.usageToString(attributes1.getSystemUsage());
             assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
+        }
 
-            requestResult = mAudioManager.requestAudioFocus(audioFocusRequest2);
-            message = "Focus gain request failed for 2nd "
+        // If exclusive we expect to lose focus on 1st one
+        // unless we have a concurrent interaction
+        if (interaction == INTERACTION_EXCLUSIVE || interaction == INTERACTION_CONCURRENT) {
+            message = "Focus change was not dispatched for 1st "
+                    + AudioAttributes.usageToString(attributes1.getSystemUsage());
+            boolean shouldStop = false;
+            int counter = 0;
+            while (!shouldStop && counter++ < TEST_TORELANCE_MAX_ITERATIONS) {
+                boolean gainedFocusLoss = focusChangeListener1.waitForFocusChangeAndAssertFocus(
+                        TEST_TIMING_TOLERANCE_MS, expectedLoss, message);
+                shouldStop = gainedFocusLoss
+                        || (expectedLoss == AudioManager.AUDIOFOCUS_NONE);
+            }
+            assertThat(shouldStop).isTrue();
+            focusChangeListener1.resetFocusChangeAndWait();
+
+            if (expectedLoss == AudioManager.AUDIOFOCUS_LOSS) {
+                mAudioFocusRequestsSet.remove(audioFocusRequest1);
+            }
+            requestResult = mAudioManager.abandonAudioFocusRequest(audioFocusRequest2);
+            mAudioFocusRequestsSet.remove(audioFocusRequest2);
+            message = "Focus loss request failed  for 2nd "
                     + AudioAttributes.usageToString(attributes2.getSystemUsage());
-            assertEquals(message, secondRequestResultsExpected, requestResult);
+            assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
 
-            // If the results is rejected for second one we only have to clean up first
-            // as the second focus request is rejected
-            if (interaction == INTERACTION_REJECT) {
-                requestResult = mAudioManager.abandonAudioFocusRequest(audioFocusRequest1);
-                focusRequests[0] = null;
-                message = "Focus loss request failed for 1st "
-                        + AudioAttributes.usageToString(attributes1.getSystemUsage());
-                assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
-            }
 
-            // If exclusive we expect to lose focus on 1st one
-            // unless we have a concurrent interaction
-            if (interaction == INTERACTION_EXCLUSIVE || interaction == INTERACTION_CONCURRENT) {
-                message = "Focus change was not dispatched for 1st "
-                        + AudioAttributes.usageToString(attributes1.getSystemUsage());
-                boolean shouldStop = false;
-                int counter = 0;
-                while (!shouldStop && counter++ < TEST_TORELANCE_MAX_ITERATIONS) {
-                    boolean gainedFocusLoss = focusChangeListener1.waitForFocusChangeAndAssertFocus(
-                            TEST_TIMING_TOLERANCE_MS, expectedLoss, message);
-                    shouldStop = gainedFocusLoss
-                            || (expectedLoss == AudioManager.AUDIOFOCUS_NONE);
-                }
-                assertThat(shouldStop).isTrue();
-                focusChangeListener1.resetFocusChangeAndWait();
+            // If the loss was transient then we should have received back on 1st
+            if ((gainType == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+                    || gainType == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)) {
 
-                requestResult = mAudioManager.abandonAudioFocusRequest(audioFocusRequest2);
-                focusRequests[1] = null;
-                message = "Focus loss request failed  for 2nd "
-                        + AudioAttributes.usageToString(attributes2.getSystemUsage());
-                assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
-
-                // If the loss was transient then we should have received back on 1st
-                if ((gainType == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
-                        || gainType == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)) {
-
-                    // Since ducking and concurrent can exist together
-                    // this needs to be skipped as the focus lost is not sent
-                    if (!(interaction == INTERACTION_CONCURRENT
-                            && gainType == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)) {
-                        message = "Focus change was not dispatched for 1st "
-                                + AudioAttributes.usageToString(attributes1.getSystemUsage());
-
-                        boolean focusGained = false;
-                        int count = 0;
-                        while (!focusGained && count++ < TEST_TORELANCE_MAX_ITERATIONS) {
-                            focusGained = focusChangeListener1.waitForFocusChangeAndAssertFocus(
-                                    TEST_TIMING_TOLERANCE_MS,
-                                    AudioManager.AUDIOFOCUS_GAIN, message);
-                        }
-                        assertThat(focusGained).isTrue();
-                        focusChangeListener1.resetFocusChangeAndWait();
-                    }
-                    // For concurrent focus interactions still needs to be released
-                    message = "Focus loss request failed  for 1st  "
+                // Since ducking and concurrent can exist together
+                // this needs to be skipped as the focus lost is not sent
+                if (!(interaction == INTERACTION_CONCURRENT
+                        && gainType == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)) {
+                    message = "Focus change was not dispatched for 1st "
                             + AudioAttributes.usageToString(attributes1.getSystemUsage());
-                    requestResult = mAudioManager.abandonAudioFocusRequest(audioFocusRequest1);
-                    focusRequests[0] = null;
-                    assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED,
-                            requestResult);
+
+                    boolean focusGained = false;
+                    int count = 0;
+                    while (!focusGained && count++ < TEST_TORELANCE_MAX_ITERATIONS) {
+                        focusGained = focusChangeListener1.waitForFocusChangeAndAssertFocus(
+                                TEST_TIMING_TOLERANCE_MS,
+                                AudioManager.AUDIOFOCUS_GAIN, message);
+                    }
+                    assertThat(focusGained).isTrue();
+                    focusChangeListener1.resetFocusChangeAndWait();
                 }
-            }
-        } finally {
-            for (AudioFocusRequest focusRequest: focusRequests) {
-                if (focusRequest != null) {
-                    mAudioManager.abandonAudioFocusRequest(focusRequest);
-                }
+                // For concurrent focus interactions still needs to be released
+                message = "Focus loss request failed  for 1st  "
+                        + AudioAttributes.usageToString(attributes1.getSystemUsage());
+                requestResult = mAudioManager.abandonAudioFocusRequest(audioFocusRequest1);
+                mAudioFocusRequestsSet.remove(audioFocusRequest1);
+                assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED,
+                        requestResult);
             }
         }
     }
@@ -610,10 +620,9 @@
      * Verifies usage can request audio focus and release it
      *
      * @param attribute usage attribute to request focus
-     * @throws Exception
      */
-    private void requestAndLoseFocusForAttribute(AudioAttributes attribute) throws Exception {
-        final FocusChangeListener focusChangeListener = new FocusChangeListener();
+    private void requestAndLoseFocusForAttribute(AudioAttributes attribute) {
+        FocusChangeListener focusChangeListener = new FocusChangeListener();
         AudioFocusRequest audioFocusRequest = new AudioFocusRequest
                 .Builder(AudioManager.AUDIOFOCUS_GAIN)
                 .setAudioAttributes(attribute)
@@ -622,30 +631,25 @@
                 .build();
 
 
-        try {
-            int requestResult = mAudioManager.requestAudioFocus(audioFocusRequest);
-            String message = "Focus gain request failed  for "
-                    + AudioAttributes.usageToString(attribute.getSystemUsage());
-            assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
+        int requestResult = mAudioManager.requestAudioFocus(audioFocusRequest);
+        String message = "Focus gain request failed  for "
+                + AudioAttributes.usageToString(attribute.getSystemUsage());
+        assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
+        mAudioFocusRequestsSet.add(audioFocusRequest);
 
-            // Verify no focus changed dispatched
-            message = "Focus change was dispatched for "
-                    + AudioAttributes.usageToString(attribute.getSystemUsage());
+        // Verify no focus changed dispatched
+        message = "Focus change was dispatched for "
+                + AudioAttributes.usageToString(attribute.getSystemUsage());
 
-            assertThat(focusChangeListener.waitForFocusChangeAndAssertFocus(
-                    TEST_TIMING_TOLERANCE_MS, AudioManager.AUDIOFOCUS_NONE, message)).isFalse();
-            focusChangeListener.resetFocusChangeAndWait();
+        assertThat(focusChangeListener.waitForFocusChangeAndAssertFocus(
+                TEST_TIMING_TOLERANCE_MS, AudioManager.AUDIOFOCUS_NONE, message)).isFalse();
+        focusChangeListener.resetFocusChangeAndWait();
 
-            requestResult = mAudioManager.abandonAudioFocusRequest(audioFocusRequest);
-            audioFocusRequest = null;
-            message = "Focus loss request failed  for "
-                    + AudioAttributes.usageToString(attribute.getSystemUsage());
-            assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
-        } finally {
-            if (audioFocusRequest != null) {
-                mAudioManager.abandonAudioFocusRequest(audioFocusRequest);
-            }
-        }
+        requestResult = mAudioManager.abandonAudioFocusRequest(audioFocusRequest);
+        message = "Focus loss request failed  for "
+                + AudioAttributes.usageToString(attribute.getSystemUsage());
+        assertEquals(message, AudioManager.AUDIOFOCUS_REQUEST_GRANTED, requestResult);
+        mAudioFocusRequestsSet.remove(audioFocusRequest);
     }
 
     private static class FocusChangeListener implements AudioManager.OnAudioFocusChangeListener {
diff --git a/tests/carservice_unit_test/src/android/car/AbstractExtendedMockitoCarServiceTestCase.java b/tests/carservice_unit_test/src/android/car/AbstractExtendedMockitoCarServiceTestCase.java
new file mode 100644
index 0000000..258e49e
--- /dev/null
+++ b/tests/carservice_unit_test/src/android/car/AbstractExtendedMockitoCarServiceTestCase.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package android.car;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+import android.annotation.NonNull;
+import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.util.Log;
+
+import com.android.car.CarLocalServices;
+
+/**
+ * Specialized {@link AbstractExtendedMockitoTestCase} implementation that provides
+ * CarService-related helpers.
+ */
+public abstract class AbstractExtendedMockitoCarServiceTestCase
+        extends AbstractExtendedMockitoTestCase {
+
+    private static final String TAG = AbstractExtendedMockitoCarServiceTestCase.class
+            .getSimpleName();
+
+    private static final boolean VERBOSE = false;
+
+    /**
+     * Mocks a call to {@link CarLocalServices#getService(Class)}.
+     *
+     * @throws IllegalStateException if class didn't override {@link #newSessionBuilder()} and
+     * called {@code spyStatic(CarLocalServices.class)} on the session passed to it.
+     */
+    protected final <T> void mockGetCarLocalService(@NonNull Class<T> type, @NonNull T service) {
+        if (VERBOSE) Log.v(TAG, getLogPrefix() + "mockGetLocalService(" + type.getName() + ")");
+        assertSpied(CarLocalServices.class);
+
+        beginTrace("mockGetLocalService");
+        doReturn(service).when(() -> CarLocalServices.getService(type));
+        endTrace();
+    }
+}
diff --git a/tests/carservice_unit_test/src/android/car/userlib/InitialUserSetterTest.java b/tests/carservice_unit_test/src/android/car/userlib/InitialUserSetterTest.java
index f61b19a..785cbc4 100644
--- a/tests/carservice_unit_test/src/android/car/userlib/InitialUserSetterTest.java
+++ b/tests/carservice_unit_test/src/android/car/userlib/InitialUserSetterTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
@@ -41,12 +42,16 @@
 import android.app.ActivityManager;
 import android.app.IActivityManager;
 import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.car.userlib.InitialUserSetter.Builder;
+import android.car.userlib.InitialUserSetter.InitialUserInfo;
+import android.content.Context;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
 import android.hardware.automotive.vehicle.V2_0.UserFlags;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 
 import com.android.internal.widget.LockPatternUtils;
 
@@ -69,6 +74,9 @@
     private static final int CURRENT_USER_ID = 12;
 
     @Mock
+    private Context mContext;
+
+    @Mock
     private CarUserManagerHelper mHelper;
 
     @Mock
@@ -94,20 +102,53 @@
 
     @Before
     public void setFixtures() {
-        mSetter = spy(new InitialUserSetter(mHelper, mUm, mListener,
-                mLockPatternUtils, OWNER_NAME, GUEST_NAME,
-                /* supportsOverrideUserIdProperty= */ false));
+        mSetter = spy(new InitialUserSetter(mContext, mHelper, mUm, mListener,
+                mLockPatternUtils, OWNER_NAME, GUEST_NAME));
 
         doReturn(mIActivityManager).when(() -> ActivityManager.getService());
         mockGetCurrentUser(CURRENT_USER_ID);
     }
 
     @Test
+    public void testSet_null() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mSetter.set(null));
+    }
+
+    @Test
+    public void testInitialUserInfoBuilder_invalidType() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> new InitialUserSetter.Builder(-1));
+    }
+
+    @Test
+    public void testInitialUserInfoBuilder_invalidSetSwitchUserId() throws Exception {
+        InitialUserSetter.Builder builder = new InitialUserSetter.Builder(
+                InitialUserSetter.TYPE_CREATE);
+        assertThrows(IllegalArgumentException.class, () -> builder.setSwitchUserId(USER_ID));
+    }
+
+    @Test
+    public void testInitialUserInfoBuilder_invalidSetNewUserName() throws Exception {
+        InitialUserSetter.Builder builder = new InitialUserSetter.Builder(
+                InitialUserSetter.TYPE_SWITCH);
+        assertThrows(IllegalArgumentException.class, () -> builder.setNewUserName(OWNER_NAME));
+    }
+
+    @Test
+    public void testInitialUserInfoBuilder_invalidSetNewUserFlags() throws Exception {
+        InitialUserSetter.Builder builder = new InitialUserSetter.Builder(
+                InitialUserSetter.TYPE_SWITCH);
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setNewUserFlags(UserFlags.ADMIN));
+    }
+
+    @Test
     public void testSwitchUser_ok_nonGuest() throws Exception {
         UserInfo user = expectUserExists(USER_ID);
         expectSwitchUser(USER_ID);
 
-        mSetter.switchUser(USER_ID, /* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(USER_ID)
+                .build());
 
         verifyUserSwitched(USER_ID);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -120,7 +161,9 @@
         UserInfo user = expectUserExists(UserHandle.USER_SYSTEM);
         expectSwitchUser(UserHandle.USER_SYSTEM);
 
-        mSetter.switchUser(UserHandle.USER_SYSTEM, true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(UserHandle.USER_SYSTEM)
+                .build());
 
         verifyUserSwitched(UserHandle.USER_SYSTEM);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -137,7 +180,10 @@
         expectGuestReplaced(USER_ID, newGuest);
         expectSwitchUser(NEW_USER_ID);
 
-        mSetter.switchUser(USER_ID, /* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(USER_ID)
+                .setReplaceGuest(true)
+                .build());
 
         verifyUserSwitched(NEW_USER_ID);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -152,7 +198,10 @@
         UserInfo existingGuest = expectGuestExists(USER_ID, ephemeral);
         expectSwitchUser(USER_ID);
 
-        mSetter.switchUser(USER_ID, /* replaceGuest= */ false);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(USER_ID)
+                .setReplaceGuest(false)
+                .build());
 
         verifyUserSwitched(USER_ID);
         verifyGuestNeverMarkedForDeletion();
@@ -161,12 +210,16 @@
         assertInitialUserSet(existingGuest);
     }
 
+
     @Test
     public void testSwitchUser_fail_guestReplacementFailed() throws Exception {
         expectGuestExists(USER_ID, /* isEphemeral= */ true); // ephemeral doesn't matter
         expectGuestReplaced(USER_ID, /* newGuest= */ null);
 
-        mSetter.switchUser(USER_ID, /* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(USER_ID)
+                .setReplaceGuest(true)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
@@ -178,7 +231,9 @@
         expectUserExists(USER_ID);
         expectSwitchUserFails(USER_ID);
 
-        mSetter.switchUser(USER_ID, /* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(USER_ID)
+                .build());
 
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
         verifySystemUserUnlocked();
@@ -189,7 +244,9 @@
     public void testSwitchUser_fail_userDoesntExist() throws Exception {
         // No need to set user exists expectation / will return null by default
 
-        mSetter.switchUser(USER_ID, /* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(USER_ID)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
@@ -201,7 +258,9 @@
         expectUserExists(USER_ID);
         expectSwitchUserThrowsException(USER_ID);
 
-        mSetter.switchUser(USER_ID, /* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(USER_ID)
+                .build());
 
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
         verifySystemUserUnlocked();
@@ -213,7 +272,9 @@
         mockGetCurrentUser(CURRENT_USER_ID);
         UserInfo currentUser = expectUserExists(CURRENT_USER_ID);
 
-        mSetter.switchUser(CURRENT_USER_ID, true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_SWITCH)
+                .setSwitchUserId(CURRENT_USER_ID)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -281,7 +342,10 @@
         UserInfo newUser = expectCreateFullUser(USER_ID, "TheDude", NO_FLAGS);
         expectSwitchUser(USER_ID);
 
-        mSetter.createUser("TheDude", UserFlags.NONE);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.NONE)
+                .build());
 
         verifyUserSwitched(USER_ID);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -294,7 +358,10 @@
         UserInfo newUser = expectCreateFullUser(USER_ID, "TheDude", UserInfo.FLAG_ADMIN);
         expectSwitchUser(USER_ID);
 
-        mSetter.createUser("TheDude", UserFlags.ADMIN);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.ADMIN)
+                .build());
 
         verifyUserSwitched(USER_ID);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -303,11 +370,33 @@
     }
 
     @Test
+    public void testCreateUser_ok_admin_setLocale() throws Exception {
+        UserInfo newUser = expectCreateFullUser(USER_ID, "TheDude", UserInfo.FLAG_ADMIN);
+        expectSwitchUser(USER_ID);
+
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.ADMIN)
+                .setUserLocales("LOL")
+                .build());
+
+        verifyUserSwitched(USER_ID);
+        verifyFallbackDefaultBehaviorNeverCalled();
+        verifySystemUserUnlocked();
+        assertInitialUserSet(newUser);
+        assertSystemLocales("LOL");
+    }
+
+
+    @Test
     public void testCreateUser_ok_ephemeralGuest() throws Exception {
         UserInfo newGuest = expectCreateGuestUser(USER_ID, "TheDude", UserInfo.FLAG_EPHEMERAL);
         expectSwitchUser(USER_ID);
 
-        mSetter.createUser("TheDude", UserFlags.EPHEMERAL | UserFlags.GUEST);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.EPHEMERAL | UserFlags.GUEST)
+                .build());
 
         verifyUserSwitched(USER_ID);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -319,7 +408,10 @@
     public void testCreateUser_fail_systemUser() throws Exception {
         // No need to set mUm.createUser() expectation - it shouldn't be called
 
-        mSetter.createUser("TheDude", UserFlags.SYSTEM);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.SYSTEM)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
@@ -330,7 +422,10 @@
     public void testCreateUser_fail_guestAdmin() throws Exception {
         // No need to set mUm.createUser() expectation - it shouldn't be called
 
-        mSetter.createUser("TheDude", UserFlags.GUEST | UserFlags.ADMIN);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.GUEST | UserFlags.ADMIN)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
@@ -340,7 +435,10 @@
     public void testCreateUser_fail_ephemeralAdmin() throws Exception {
         // No need to set mUm.createUser() expectation - it shouldn't be called
 
-        mSetter.createUser("TheDude", UserFlags.EPHEMERAL | UserFlags.ADMIN);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.EPHEMERAL | UserFlags.ADMIN)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
@@ -350,7 +448,10 @@
     public void testCreateUser_fail_createFail() throws Exception {
         // No need to set mUm.createUser() expectation - it will return false by default
 
-        mSetter.createUser("TheDude", UserFlags.NONE);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.NONE)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
@@ -360,7 +461,10 @@
     public void testCreateUser_fail_createThrowsException() throws Exception {
         expectCreateUserThrowsException("TheDude", UserFlags.NONE);
 
-        mSetter.createUser("TheDude", UserFlags.NONE);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.NONE)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
@@ -371,7 +475,10 @@
         expectCreateFullUser(USER_ID, "TheDude", NO_FLAGS);
         expectSwitchUserFails(USER_ID);
 
-        mSetter.createUser("TheDude", UserFlags.NONE);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_CREATE)
+                .setNewUserName("TheDude")
+                .setNewUserFlags(UserFlags.NONE)
+                .build());
 
         verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch();
         verifySystemUserUnlocked();
@@ -384,7 +491,7 @@
         UserInfo newUser = expectCreateFullUser(USER_ID, OWNER_NAME, UserInfo.FLAG_ADMIN);
         expectSwitchUser(USER_ID);
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR).build());
 
         verifyUserSwitched(USER_ID);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -393,11 +500,28 @@
     }
 
     @Test
+    public void testDefaultBehavior_firstBoot_ok_setLocale() throws Exception {
+        // no need to mock hasInitialUser(), it will return false by default
+        UserInfo newUser = expectCreateFullUser(USER_ID, OWNER_NAME, UserInfo.FLAG_ADMIN);
+        expectSwitchUser(USER_ID);
+
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+                .setUserLocales("LOL")
+                .build());
+
+        verifyUserSwitched(USER_ID);
+        verifyFallbackDefaultBehaviorNeverCalled();
+        verifySystemUserUnlocked();
+        assertInitialUserSet(newUser);
+        assertSystemLocales("LOL");
+    }
+
+    @Test
     public void testDefaultBehavior_firstBoot_fail_createUserFailed() throws Exception {
         // no need to mock hasInitialUser(), it will return false by default
         // no need to mock createUser(), it will return null by default
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR).build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromDefaultBehavior();
@@ -410,7 +534,7 @@
         expectCreateFullUser(USER_ID, OWNER_NAME, UserInfo.FLAG_ADMIN);
         expectSwitchUserFails(USER_ID);
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR).build());
 
         verifyFallbackDefaultBehaviorCalledFromDefaultBehavior();
         verifySystemUserUnlocked();
@@ -418,11 +542,27 @@
     }
 
     @Test
+    public void testDefaultBehavior_firstBoot_fail_switchFailed_setLocale() throws Exception {
+        // no need to mock hasInitialUser(), it will return false by default
+        expectCreateFullUser(USER_ID, OWNER_NAME, UserInfo.FLAG_ADMIN);
+        expectSwitchUserFails(USER_ID);
+
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+                .setUserLocales("LOL")
+                .build());
+
+        verifyFallbackDefaultBehaviorCalledFromDefaultBehavior();
+        verifySystemUserUnlocked();
+        verifyLastActiveUserNeverSet();
+        assertSystemLocales("LOL");
+    }
+
+    @Test
     public void testDefaultBehavior_nonFirstBoot_ok() throws Exception {
         UserInfo existingUser = expectHasInitialUser(USER_ID);
         expectSwitchUser(USER_ID);
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR).build());
 
         verifyUserSwitched(USER_ID);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -436,7 +576,7 @@
         UserInfo currentUser = expectHasInitialUser(CURRENT_USER_ID);
         expectSwitchUser(CURRENT_USER_ID);
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR).build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -450,7 +590,7 @@
         expectHasInitialUser(USER_ID);
         expectSwitchUserFails(USER_ID);
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR).build());
 
         verifyFallbackDefaultBehaviorCalledFromDefaultBehavior();
         verifyUserNeverCreated();
@@ -467,7 +607,9 @@
         expectGuestReplaced(USER_ID, newGuest);
         expectSwitchUser(NEW_USER_ID);
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+                .setReplaceGuest(true)
+                .build());
 
         verifyUserSwitched(NEW_USER_ID);
         verifyFallbackDefaultBehaviorNeverCalled();
@@ -483,7 +625,9 @@
         expectGuestExists(USER_ID, /* isEphemeral= */ true); // ephemeral doesn't matter
         expectGuestReplaced(USER_ID, /* newGuest= */ null);
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+                .setReplaceGuest(true)
+                .build());
 
         verifyUserNeverSwitched();
         verifyFallbackDefaultBehaviorCalledFromDefaultBehavior();
@@ -494,18 +638,17 @@
     @Test
     public void testDefaultBehavior_nonFirstBoot_ok_withOverriddenProperty() throws Exception {
         boolean supportsOverrideUserIdProperty = true;
-        // Must use a different helper as the property is set on constructor
-        InitialUserSetter setter = spy(new InitialUserSetter(mHelper, mUm, mListener,
-                mLockPatternUtils, OWNER_NAME, GUEST_NAME, supportsOverrideUserIdProperty));
         UserInfo user = expectHasInitialUser(USER_ID, supportsOverrideUserIdProperty);
-        expectSwitchUser(setter, USER_ID);
+        expectSwitchUser(USER_ID);
 
-        setter.executeDefaultBehavior(/* replaceGuest= */ true);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+                .setSupportsOverrideUserIdProperty(true)
+                .build());
 
-        verifyUserSwitched(setter, USER_ID);
-        verifyFallbackDefaultBehaviorNeverCalled();
+        verifyUserSwitched(USER_ID);
+        verifyFallbackDefaultBehaviorNeverCalled(supportsOverrideUserIdProperty);
         verifyUserNeverCreated();
-        verifySystemUserUnlocked(setter);
+        verifySystemUserUnlocked();
         assertInitialUserSet(user);
     }
 
@@ -516,7 +659,9 @@
         UserInfo existingGuest = expectHasInitialGuest(USER_ID);
         expectSwitchUser(USER_ID);
 
-        mSetter.executeDefaultBehavior(/* replaceGuest= */ false);
+        mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+                .setReplaceGuest(false)
+                .build());
 
         verifyUserSwitched(USER_ID);
         verifyGuestNeverMarkedForDeletion();
@@ -633,12 +778,7 @@
     }
 
     private void expectSwitchUser(@UserIdInt int userId) throws Exception {
-        expectSwitchUser(mSetter, userId);
-    }
-
-    private void expectSwitchUser(@NonNull InitialUserSetter setter, @UserIdInt int userId)
-            throws Exception {
-        doReturn(true).when(setter).startForegroundUser(userId);
+        doReturn(true).when(mSetter).startForegroundUser(userId);
     }
     private void expectSwitchUserFails(@UserIdInt int userId) {
         when(mSetter.startForegroundUser(userId)).thenReturn(false);
@@ -683,12 +823,7 @@
     }
 
     private void verifyUserSwitched(@UserIdInt int userId) throws Exception {
-        verifyUserSwitched(mSetter, userId);
-    }
-
-    private void verifyUserSwitched(@NonNull InitialUserSetter setter, @UserIdInt int userId)
-            throws Exception {
-        verify(setter).startForegroundUser(userId);
+        verify(mSetter).startForegroundUser(userId);
         verify(mHelper).setLastActiveUser(userId);
     }
 
@@ -714,25 +849,32 @@
     }
 
     private void verifyFallbackDefaultBehaviorCalledFromCreateOrSwitch() {
-        verify(mSetter).fallbackDefaultBehavior(eq(true), anyString());
+        verify(mSetter).fallbackDefaultBehavior(isInitialInfo(false), eq(true), anyString());
         assertInitialUserSet(null);
     }
 
     private void verifyFallbackDefaultBehaviorCalledFromDefaultBehavior() {
-        verify(mSetter).fallbackDefaultBehavior(eq(false), anyString());
+        verify(mSetter).fallbackDefaultBehavior(isInitialInfo(false), eq(false), anyString());
         assertInitialUserSet(null);
     }
 
     private void verifyFallbackDefaultBehaviorNeverCalled() {
-        verify(mSetter, never()).fallbackDefaultBehavior(anyBoolean(), anyString());
+        verifyFallbackDefaultBehaviorNeverCalled(/* supportsOverrideUserIdProperty= */ false);
+    }
+
+    private void verifyFallbackDefaultBehaviorNeverCalled(boolean supportsOverrideUserIdProperty) {
+        verify(mSetter, never()).fallbackDefaultBehavior(
+                isInitialInfo(supportsOverrideUserIdProperty), anyBoolean(), anyString());
+    }
+
+    private static InitialUserInfo isInitialInfo(boolean supportsOverrideUserIdProperty) {
+        return argThat((info) -> {
+            return info.supportsOverrideUserIdProperty == supportsOverrideUserIdProperty;
+        });
     }
 
     private void verifySystemUserUnlocked() {
-        verifySystemUserUnlocked(mSetter);
-    }
-
-    private void verifySystemUserUnlocked(InitialUserSetter setter) {
-        verify(setter).unlockSystemUser();
+        verify(mSetter).unlockSystemUser();
     }
 
     private void verifySystemUserNeverUnlocked() {
@@ -750,6 +892,11 @@
             .isSameAs(expectedUser);
     }
 
+    private void assertSystemLocales(@NonNull String expected) {
+        // TODO(b/156033195): should test specific userId
+        assertThat(getSettingsString(Settings.System.SYSTEM_LOCALES)).isEqualTo(expected);
+    }
+
     private final class MyListener implements Consumer<UserInfo> {
         public int numberCalls;
         public UserInfo initialUser;
diff --git a/tests/carservice_unit_test/src/android/car/userlib/UserHalHelperTest.java b/tests/carservice_unit_test/src/android/car/userlib/UserHalHelperTest.java
index 33a852a..84f8bd6 100644
--- a/tests/carservice_unit_test/src/android/car/userlib/UserHalHelperTest.java
+++ b/tests/carservice_unit_test/src/android/car/userlib/UserHalHelperTest.java
@@ -39,6 +39,8 @@
 import android.annotation.NonNull;
 import android.content.pm.UserInfo;
 import android.hardware.automotive.vehicle.V2_0.InitialUserInfoRequestType;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponse;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
 import android.hardware.automotive.vehicle.V2_0.UserFlags;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociation;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationType;
@@ -401,7 +403,7 @@
     }
 
     @Test
-    public void testToUserIdentificationGetResponse_invalidPropType() {
+    public void testToUserIdentificationResponse_invalidPropType() {
         VehiclePropValue prop = new VehiclePropValue();
 
         assertThrows(IllegalArgumentException.class,
@@ -485,6 +487,228 @@
     }
 
     @Test
+    public void testToInitialUserInfoResponse_null() {
+        assertThrows(NullPointerException.class,
+                () -> UserHalHelper.toInitialUserInfoResponse(null));
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_invalidPropType() {
+        VehiclePropValue prop = new VehiclePropValue();
+
+        assertThrows(IllegalArgumentException.class,
+                () -> UserHalHelper.toInitialUserInfoResponse(prop));
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_invalidSize() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        //      need at least 2: request_id, action_type
+        prop.value.int32Values.add(42);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> UserHalHelper.toInitialUserInfoResponse(prop));
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_invalidRequest() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(0);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> UserHalHelper.toInitialUserInfoResponse(prop));
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_invalidAction() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(-1); // InitialUserInfoResponseAction
+
+        assertThrows(IllegalArgumentException.class,
+                () -> UserHalHelper.toInitialUserInfoResponse(prop));
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_default_ok_noStringValue() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.DEFAULT);
+
+        InitialUserInfoResponse response = UserHalHelper.toInitialUserInfoResponse(prop);
+
+        assertThat(response).isNotNull();
+        assertThat(response.requestId).isEqualTo(42);
+        assertThat(response.action).isEqualTo(InitialUserInfoResponseAction.DEFAULT);
+        assertThat(response.userNameToCreate).isEmpty();
+        assertThat(response.userToSwitchOrCreate.userId).isEqualTo(UserHandle.USER_NULL);
+        assertThat(response.userToSwitchOrCreate.flags).isEqualTo(UserFlags.NONE);
+        assertThat(response.userLocales).isEmpty();
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_default_ok_stringValueWithJustSeparator() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.DEFAULT);
+        prop.value.stringValue = "||";
+
+        InitialUserInfoResponse response = UserHalHelper.toInitialUserInfoResponse(prop);
+
+        assertThat(response).isNotNull();
+        assertThat(response.requestId).isEqualTo(42);
+        assertThat(response.action).isEqualTo(InitialUserInfoResponseAction.DEFAULT);
+        assertThat(response.userNameToCreate).isEmpty();
+        assertThat(response.userToSwitchOrCreate.userId).isEqualTo(UserHandle.USER_NULL);
+        assertThat(response.userToSwitchOrCreate.flags).isEqualTo(UserFlags.NONE);
+        assertThat(response.userLocales).isEmpty();
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_default_ok_stringValueWithLocale() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.DEFAULT);
+        prop.value.stringValue = "esperanto,klingon";
+
+        InitialUserInfoResponse response = UserHalHelper.toInitialUserInfoResponse(prop);
+
+        assertThat(response).isNotNull();
+        assertThat(response.requestId).isEqualTo(42);
+        assertThat(response.action).isEqualTo(InitialUserInfoResponseAction.DEFAULT);
+        assertThat(response.userNameToCreate).isEmpty();
+        assertThat(response.userToSwitchOrCreate.userId).isEqualTo(UserHandle.USER_NULL);
+        assertThat(response.userToSwitchOrCreate.flags).isEqualTo(UserFlags.NONE);
+        assertThat(response.userLocales).isEqualTo("esperanto,klingon");
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_default_ok_stringValueWithLocaleWithHalfSeparator() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.DEFAULT);
+        prop.value.stringValue = "esperanto|klingon";
+
+        InitialUserInfoResponse response = UserHalHelper.toInitialUserInfoResponse(prop);
+
+        assertThat(response).isNotNull();
+        assertThat(response.requestId).isEqualTo(42);
+        assertThat(response.action).isEqualTo(InitialUserInfoResponseAction.DEFAULT);
+        assertThat(response.userNameToCreate).isEmpty();
+        assertThat(response.userToSwitchOrCreate.userId).isEqualTo(UserHandle.USER_NULL);
+        assertThat(response.userToSwitchOrCreate.flags).isEqualTo(UserFlags.NONE);
+        assertThat(response.userLocales).isEqualTo("esperanto|klingon");
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_switch_missingUserId() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.SWITCH);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> UserHalHelper.toInitialUserInfoResponse(prop));
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_switch_ok_noLocale() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.SWITCH);
+        prop.value.int32Values.add(108); // user id
+
+        InitialUserInfoResponse response = UserHalHelper.toInitialUserInfoResponse(prop);
+
+        assertThat(response).isNotNull();
+        assertThat(response.requestId).isEqualTo(42);
+        assertThat(response.action).isEqualTo(InitialUserInfoResponseAction.SWITCH);
+        assertThat(response.userNameToCreate).isEmpty();
+        assertThat(response.userToSwitchOrCreate.userId).isEqualTo(108);
+        assertThat(response.userToSwitchOrCreate.flags).isEqualTo(UserFlags.NONE);
+        assertThat(response.userLocales).isEmpty();
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_switch_ok_withLocale() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.SWITCH);
+        prop.value.int32Values.add(108); // user id
+        // add some extra | to make sure they're ignored
+        prop.value.stringValue = "esperanto,klingon|||";
+        InitialUserInfoResponse response = UserHalHelper.toInitialUserInfoResponse(prop);
+
+        assertThat(response).isNotNull();
+        assertThat(response.requestId).isEqualTo(42);
+        assertThat(response.action).isEqualTo(InitialUserInfoResponseAction.SWITCH);
+        assertThat(response.userNameToCreate).isEmpty();
+        assertThat(response.userToSwitchOrCreate.userId).isEqualTo(108);
+        assertThat(response.userToSwitchOrCreate.flags).isEqualTo(UserFlags.NONE);
+        assertThat(response.userLocales).isEqualTo("esperanto,klingon");
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_create_missingFlags() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.CREATE);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> UserHalHelper.toInitialUserInfoResponse(prop));
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_create_ok_noLocale() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.CREATE);
+        prop.value.int32Values.add(UserFlags.GUEST);
+        prop.value.stringValue = "||ElGuesto";
+
+        InitialUserInfoResponse response = UserHalHelper.toInitialUserInfoResponse(prop);
+
+        assertThat(response).isNotNull();
+        assertThat(response.requestId).isEqualTo(42);
+        assertThat(response.action).isEqualTo(InitialUserInfoResponseAction.CREATE);
+        assertThat(response.userNameToCreate).isEqualTo("ElGuesto");
+        assertThat(response.userToSwitchOrCreate.userId).isEqualTo(UserHandle.USER_NULL);
+        assertThat(response.userToSwitchOrCreate.flags).isEqualTo(UserFlags.GUEST);
+        assertThat(response.userLocales).isEmpty();
+    }
+
+    @Test
+    public void testToInitialUserInfoResponse_create_ok_withLocale() {
+        VehiclePropValue prop = new VehiclePropValue();
+        prop.prop = UserHalHelper.INITIAL_USER_INFO_PROPERTY;
+        prop.value.int32Values.add(42); // request id
+        prop.value.int32Values.add(InitialUserInfoResponseAction.CREATE);
+        prop.value.int32Values.add(UserFlags.GUEST);
+        prop.value.stringValue = "esperanto,klingon||ElGuesto";
+
+        InitialUserInfoResponse response = UserHalHelper.toInitialUserInfoResponse(prop);
+
+        assertThat(response).isNotNull();
+        assertThat(response.requestId).isEqualTo(42);
+        assertThat(response.action).isEqualTo(InitialUserInfoResponseAction.CREATE);
+        assertThat(response.userNameToCreate).isEqualTo("ElGuesto");
+        assertThat(response.userToSwitchOrCreate.userId).isEqualTo(UserHandle.USER_NULL);
+        assertThat(response.userToSwitchOrCreate.flags).isEqualTo(UserFlags.GUEST);
+        assertThat(response.userLocales).isEqualTo("esperanto,klingon");
+    }
+
+    @Test
     public void testUserIdentificationSetRequestToVehiclePropValue_null() {
         assertThrows(NullPointerException.class,
                 () -> UserHalHelper.toVehiclePropValue((UserIdentificationSetRequest) null));
diff --git a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
index ad241b5..c7906be 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
@@ -26,11 +26,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
@@ -138,6 +138,8 @@
         if (mService != null) {
             mService.release();
         }
+        CarServiceUtils.runOnLooperSync(CarServiceUtils.getHandlerThread(
+                CarPowerManagementService.class.getSimpleName()).getLooper(), () -> { });
         mIOInterface.tearDown();
     }
 
@@ -584,12 +586,18 @@
     }
 
     private void verifyUserNotSwitched() {
-        verify(mInitialUserSetter, never()).switchUser(anyInt(), anyBoolean());
+        verify(mInitialUserSetter, never()).set(argThat((info) -> {
+            return info.type == InitialUserSetter.TYPE_SWITCH;
+        }));
     }
 
     private void verifyUserSwitched(int userId) {
         // TODO(b/153679319): pass proper value for replaceGuest
-        verify(mInitialUserSetter).switchUser(userId, true);
+        verify(mInitialUserSetter).set(argThat((info) -> {
+            return info.type == InitialUserSetter.TYPE_SWITCH
+                    && info.switchUserId == userId
+                    && info.replaceGuest;
+        }));
     }
 
     private void expectNewGuestCreated(int existingGuestId, UserInfo newGuest) {
@@ -599,15 +607,24 @@
 
     private void verifyDefaultInitialUserBehaviorCalled() {
         // TODO(b/153679319): pass proper value for replaceGuest
-        verify(mInitialUserSetter).executeDefaultBehavior(true);
+        verify(mInitialUserSetter).set(argThat((info) -> {
+            return info.type == InitialUserSetter.TYPE_DEFAULT_BEHAVIOR
+                    && info.replaceGuest;
+        }));
     }
 
     private void verifyDefaultInitilUserBehaviorNeverCalled() {
-        verify(mInitialUserSetter, never()).executeDefaultBehavior(anyBoolean());
+        verify(mInitialUserSetter, never()).set(argThat((info) -> {
+            return info.type == InitialUserSetter.TYPE_DEFAULT_BEHAVIOR;
+        }));
     }
 
     private void verifyUserCreated(String name, int halFlags) {
-        verify(mInitialUserSetter).createUser(name, halFlags);
+        verify(mInitialUserSetter).set(argThat((info) -> {
+            return info.type == InitialUserSetter.TYPE_CREATE
+                    && info.newUserName == name
+                    && info.newUserFlags == halFlags;
+        }));
     }
 
     private static final class MockDisplayInterface implements DisplayInterface {
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsAssociatedLayerTest.java b/tests/carservice_unit_test/src/com/android/car/VmsAssociatedLayerTest.java
deleted file mode 100644
index 1d384cc..0000000
--- a/tests/carservice_unit_test/src/com/android/car/VmsAssociatedLayerTest.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * 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 org.junit.Assert.assertNotEquals;
-
-import android.car.vms.VmsAssociatedLayer;
-import android.car.vms.VmsLayer;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import android.os.Parcel;
-
-import java.util.Arrays;
-import java.util.HashSet;
-
-
-/*
- * A class to test the VmsAssociatedLayer parcelability.
- */
-@SmallTest
-public class VmsAssociatedLayerTest extends AndroidTestCase {
-    private static final int LAYER_ID = 12;
-    private static final int LAYER_VERSION = 34;
-    private static final int LAYER_SUBTYPE = 56;
-
-    private static final int PUBLISHER_ID_1 = 111;
-    private static final int PUBLISHER_ID_2 = 222;
-
-    private static final int DIFFERENT_LAYER_ID = 99;
-
-    private static final VmsLayer VMS_LAYER = new VmsLayer(
-            LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-    private static final VmsLayer ANOTHER_VMS_LAYER = new VmsLayer(
-            DIFFERENT_LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-
-    public void testNoPublishersAssociatedLayerParcel() throws Exception {
-        VmsAssociatedLayer vmsAssociatedLayer =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList()));
-        Parcel parcel = Parcel.obtain();
-        vmsAssociatedLayer.writeToParcel(parcel, vmsAssociatedLayer.describeContents());
-        parcel.setDataPosition(0);
-        VmsAssociatedLayer createdFromParcel = VmsAssociatedLayer.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsAssociatedLayer, createdFromParcel);
-    }
-
-    public void testLayerWithMultiplePublishersParcel() throws Exception {
-        VmsAssociatedLayer vmsAssociatedLayer =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(PUBLISHER_ID_1, PUBLISHER_ID_2)));
-        Parcel parcel = Parcel.obtain();
-        vmsAssociatedLayer.writeToParcel(parcel, vmsAssociatedLayer.describeContents());
-        parcel.setDataPosition(0);
-        VmsAssociatedLayer createdFromParcel = VmsAssociatedLayer.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsAssociatedLayer, createdFromParcel);
-    }
-
-    public void testNoPublishersAssociatedLayerEquality() throws Exception {
-        VmsAssociatedLayer original =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList()));
-
-        VmsAssociatedLayer similar =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList()));
-
-        assertEquals(original.getVmsLayer(), similar.getVmsLayer());
-        assertEquals(original.getPublisherIds(), similar.getPublisherIds());
-        assertEquals(original, similar);
-    }
-
-    public void testLayerWithMultiplePublishersEquality() throws Exception {
-        VmsAssociatedLayer original =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(PUBLISHER_ID_1, PUBLISHER_ID_2)));
-
-        VmsAssociatedLayer similar =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(PUBLISHER_ID_1, PUBLISHER_ID_2)));
-
-        assertEquals(original.getVmsLayer(), similar.getVmsLayer());
-        assertEquals(original.getPublisherIds(), similar.getPublisherIds());
-        assertEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnLayer() throws Exception {
-        VmsAssociatedLayer original =
-                new VmsAssociatedLayer(
-                        ANOTHER_VMS_LAYER,
-                        new HashSet<>(Arrays.asList()));
-
-        VmsAssociatedLayer similar =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList()));
-
-        assertNotEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnPublisherIds() throws Exception {
-        VmsAssociatedLayer original =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(PUBLISHER_ID_1)));
-
-        VmsAssociatedLayer similar =
-                new VmsAssociatedLayer(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(PUBLISHER_ID_2)));
-
-        assertNotEquals(original, similar);
-    }
-}
\ No newline at end of file
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsAvailableLayersTest.java b/tests/carservice_unit_test/src/com/android/car/VmsAvailableLayersTest.java
deleted file mode 100644
index 63ac1a3..0000000
--- a/tests/carservice_unit_test/src/com/android/car/VmsAvailableLayersTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * 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 org.junit.Assert.assertNotEquals;
-
-import android.car.vms.VmsAssociatedLayer;
-import android.car.vms.VmsAvailableLayers;
-import android.car.vms.VmsLayer;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import android.os.Parcel;
-
-import java.util.Arrays;
-import java.util.HashSet;
-
-
-/*
- * A class to test the VmsAvailableLayers parcelability.
- */
-@SmallTest
-public class VmsAvailableLayersTest extends AndroidTestCase {
-    private static final int LAYER_ID = 12;
-    private static final int LAYER_VERSION = 34;
-    private static final int LAYER_SUBTYPE = 56;
-    private static final int PUBLISHER_ID = 999;
-    private static final int SEQUENCE_NUMBER = 17;
-
-    private static final int DIFFERENT_SEQUENCE_NUMBER = 71;
-    private static final int DIFFERENT_LAYER_ID = 21;
-
-    private static final VmsLayer VMS_LAYER = new VmsLayer(
-            LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-    private static final VmsLayer ANOTHER_VMS_LAYER = new VmsLayer(
-            DIFFERENT_LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-    private static final VmsAssociatedLayer VMS_ASSOCIATED_LAYER = new VmsAssociatedLayer(
-            VMS_LAYER,
-            new HashSet<>(Arrays.asList(PUBLISHER_ID)));
-
-    private static final VmsAssociatedLayer ANOTHER_VMS_ASSOCIATED_LAYER = new VmsAssociatedLayer(
-            ANOTHER_VMS_LAYER,
-            new HashSet<>(Arrays.asList(PUBLISHER_ID)));
-
-    public void testNoAvailableLayersParcel() throws Exception {
-        VmsAvailableLayers vmsAvailableLayers =
-                new VmsAvailableLayers(new HashSet<>(Arrays.asList()), SEQUENCE_NUMBER);
-
-        Parcel parcel = Parcel.obtain();
-
-        vmsAvailableLayers.writeToParcel(parcel, vmsAvailableLayers.describeContents());
-        parcel.setDataPosition(0);
-        VmsAvailableLayers createdFromParcel = VmsAvailableLayers.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsAvailableLayers, createdFromParcel);
-    }
-
-    public void testMultipleAvailableLayersParcel() throws Exception {
-        VmsAvailableLayers vmsAvailableLayers =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList(
-                                VMS_ASSOCIATED_LAYER,
-                                ANOTHER_VMS_ASSOCIATED_LAYER)),
-                        SEQUENCE_NUMBER);
-
-        Parcel parcel = Parcel.obtain();
-
-        vmsAvailableLayers.writeToParcel(parcel, vmsAvailableLayers.describeContents());
-        parcel.setDataPosition(0);
-        VmsAvailableLayers createdFromParcel = VmsAvailableLayers.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsAvailableLayers, createdFromParcel);
-    }
-
-    public void testNoAvailableLayerEquality() throws Exception {
-        VmsAvailableLayers original =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList()),
-                        SEQUENCE_NUMBER);
-
-        VmsAvailableLayers similar =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList()),
-                        SEQUENCE_NUMBER);
-
-        assertEquals(original.getAssociatedLayers(), similar.getAssociatedLayers());
-        assertEquals(original.getSequence(), similar.getSequence());
-        assertEquals(original, similar);
-    }
-
-    public void testMultipleAvailableLayerEquality() throws Exception {
-        VmsAvailableLayers original =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList(
-                                VMS_ASSOCIATED_LAYER,
-                                ANOTHER_VMS_ASSOCIATED_LAYER)),
-                        SEQUENCE_NUMBER);
-
-        VmsAvailableLayers similar =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList(
-                                VMS_ASSOCIATED_LAYER,
-                                ANOTHER_VMS_ASSOCIATED_LAYER)),
-                        SEQUENCE_NUMBER);
-
-        assertEquals(original.getAssociatedLayers(), similar.getAssociatedLayers());
-        assertEquals(original.getSequence(), similar.getSequence());
-        assertEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnAssociatedLayer() throws Exception {
-        VmsAvailableLayers original =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)),
-                        SEQUENCE_NUMBER);
-
-        VmsAvailableLayers similar =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList(ANOTHER_VMS_ASSOCIATED_LAYER)),
-                        SEQUENCE_NUMBER);
-
-        assertNotEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnSequence() throws Exception {
-        VmsAvailableLayers original =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)),
-                        SEQUENCE_NUMBER);
-
-        VmsAvailableLayers similar =
-                new VmsAvailableLayers(
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)),
-                        DIFFERENT_SEQUENCE_NUMBER);
-
-        assertNotEquals(original, similar);
-    }
-}
\ No newline at end of file
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsLayerDependencyTest.java b/tests/carservice_unit_test/src/com/android/car/VmsLayerDependencyTest.java
deleted file mode 100644
index 4c659cd..0000000
--- a/tests/carservice_unit_test/src/com/android/car/VmsLayerDependencyTest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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 org.junit.Assert.assertNotEquals;
-
-import android.car.vms.VmsLayer;
-import android.car.vms.VmsLayerDependency;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import android.os.Parcel;
-
-import java.util.Arrays;
-import java.util.HashSet;
-
-
-/*
- * A class to test the VmsLayerDependency parcelability.
- */
-@SmallTest
-public class VmsLayerDependencyTest extends AndroidTestCase {
-    private static final int LAYER_ID = 112;
-    private static final int LAYER_VERSION = 134;
-    private static final int LAYER_SUBTYPE = 156;
-
-    private static final int DEPENDENT_LAYER_ID = 212;
-    private static final int DEPENDENT_LAYER_VERSION = 234;
-    private static final int DEPENDENT_LAYER_SUBTYPE = 256;
-
-    private static final int DIFFERENT_LAYER_ID = 99;
-
-    private static final VmsLayer VMS_LAYER = new VmsLayer(
-            LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-    private static final VmsLayer ANOTHER_VMS_LAYER = new VmsLayer(
-            DIFFERENT_LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-    private static final VmsLayer DEPENDENT_VMS_LAYER = new VmsLayer(
-            DEPENDENT_LAYER_ID,
-            DEPENDENT_LAYER_SUBTYPE,
-            DEPENDENT_LAYER_VERSION);
-
-    private static final VmsLayer ANOTHER_DEPENDENT_VMS_LAYER = new VmsLayer(
-            DIFFERENT_LAYER_ID,
-            DEPENDENT_LAYER_SUBTYPE,
-            DEPENDENT_LAYER_VERSION);
-
-    public void testNoDependendtLayerParcel() throws Exception {
-        VmsLayerDependency vmsLayerDependency =
-                new VmsLayerDependency(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList()));
-
-        Parcel parcel = Parcel.obtain();
-        vmsLayerDependency.writeToParcel(parcel, vmsLayerDependency.describeContents());
-        parcel.setDataPosition(0);
-        VmsLayerDependency createdFromParcel = VmsLayerDependency.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsLayerDependency, createdFromParcel);
-    }
-
-    public void testMultipleDependendtLayerParcel() throws Exception {
-        VmsLayerDependency vmsLayerDependency =
-                new VmsLayerDependency(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(DEPENDENT_VMS_LAYER, ANOTHER_DEPENDENT_VMS_LAYER)));
-
-        Parcel parcel = Parcel.obtain();
-        vmsLayerDependency.writeToParcel(parcel, vmsLayerDependency.describeContents());
-        parcel.setDataPosition(0);
-        VmsLayerDependency createdFromParcel = VmsLayerDependency.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsLayerDependency, createdFromParcel);
-    }
-
-    public void testNoDependendtLayerEquality() throws Exception {
-        VmsLayerDependency original =
-                new VmsLayerDependency(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList()));
-
-        VmsLayerDependency similar =
-                new VmsLayerDependency(VMS_LAYER);
-
-        assertEquals(original.getLayer(), similar.getLayer());
-        assertEquals(original.getDependencies(), similar.getDependencies());
-        assertEquals(original, similar);
-    }
-
-    public void testMultipleDependendtLayerEquality() throws Exception {
-        VmsLayerDependency original =
-                new VmsLayerDependency(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(DEPENDENT_VMS_LAYER, ANOTHER_DEPENDENT_VMS_LAYER)));
-
-        VmsLayerDependency similar =
-                new VmsLayerDependency(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(DEPENDENT_VMS_LAYER, ANOTHER_DEPENDENT_VMS_LAYER)));
-        assertEquals(original.getLayer(), similar.getLayer());
-        assertEquals(original.getDependencies(), similar.getDependencies());
-        assertEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnLayer() throws Exception {
-        VmsLayerDependency original =
-                new VmsLayerDependency(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(DEPENDENT_VMS_LAYER)));
-
-        VmsLayerDependency similar =
-                new VmsLayerDependency(
-                        ANOTHER_DEPENDENT_VMS_LAYER,
-                        new HashSet<>(Arrays.asList(DEPENDENT_VMS_LAYER)));
-
-        assertNotEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnDependentLayer() throws Exception {
-        VmsLayerDependency original =
-                new VmsLayerDependency(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(DEPENDENT_VMS_LAYER)));
-
-        VmsLayerDependency similar =
-                new VmsLayerDependency(
-                        VMS_LAYER,
-                        new HashSet<>(Arrays.asList(ANOTHER_DEPENDENT_VMS_LAYER)));
-
-        assertNotEquals(original, similar);
-    }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsLayerTest.java b/tests/carservice_unit_test/src/com/android/car/VmsLayerTest.java
deleted file mode 100644
index 56fb129..0000000
--- a/tests/carservice_unit_test/src/com/android/car/VmsLayerTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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 org.junit.Assert.assertNotEquals;
-
-import android.car.vms.VmsLayer;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import android.os.Parcel;
-
-import java.util.Arrays;
-import java.util.HashSet;
-
-/*
- * A class to test the VmsLayer parcelability.
- */
-@SmallTest
-public class VmsLayerTest extends AndroidTestCase {
-    private static final int LAYER_ID = 12;
-    private static final int LAYER_VERSION = 34;
-    private static final int LAYER_SUBTYPE = 56;
-
-    private static final int DIFFERENT_LAYER_ID = 99;
-
-    public void testLayerParcel() throws Exception {
-        VmsLayer vmsLayer =
-                new VmsLayer(
-                        LAYER_ID,
-                        LAYER_SUBTYPE,
-                        LAYER_VERSION);
-
-        Parcel parcel = Parcel.obtain();
-        vmsLayer.writeToParcel(parcel, vmsLayer.describeContents());
-        parcel.setDataPosition(0);
-        VmsLayer createdFromParcel = VmsLayer.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsLayer, createdFromParcel);
-    }
-
-    public void testLayerEquality() throws Exception {
-        VmsLayer original =
-                new VmsLayer(
-                        LAYER_ID,
-                        LAYER_SUBTYPE,
-                        LAYER_VERSION);
-
-        VmsLayer similar =
-                new VmsLayer(
-                        LAYER_ID,
-                        LAYER_SUBTYPE,
-                        LAYER_VERSION);
-
-        assertEquals(original.getType(), similar.getType());
-        assertEquals(original.getSubtype(), similar.getSubtype());
-        assertEquals(original.getVersion(), similar.getVersion());
-        assertEquals(original, similar);
-    }
-
-    public void testVerifyNonEqual() throws Exception {
-        VmsLayer original =
-                new VmsLayer(
-                        LAYER_ID,
-                        LAYER_SUBTYPE,
-                        LAYER_VERSION);
-
-        VmsLayer similar =
-                new VmsLayer(
-                        DIFFERENT_LAYER_ID,
-                        LAYER_SUBTYPE,
-                        LAYER_VERSION);
-
-        assertNotEquals(original, similar);
-    }
-}
\ No newline at end of file
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsLayersOfferingTest.java b/tests/carservice_unit_test/src/com/android/car/VmsLayersOfferingTest.java
deleted file mode 100644
index c2111a9..0000000
--- a/tests/carservice_unit_test/src/com/android/car/VmsLayersOfferingTest.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * 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 org.junit.Assert.assertNotEquals;
-
-import android.car.vms.VmsLayer;
-import android.car.vms.VmsLayerDependency;
-import android.car.vms.VmsLayersOffering;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.os.Parcel;
-
-import java.util.Arrays;
-import java.util.HashSet;
-
-import android.util.Log;
-
-/*
- * A class to test the VmsLayersOffering parcelability.
- */
-@SmallTest
-public class VmsLayersOfferingTest extends AndroidTestCase {
-    private static final int PUBLISHER_ID = 1;
-
-    private static final int LAYER_ID = 112;
-    private static final int LAYER_VERSION = 134;
-    private static final int LAYER_SUBTYPE = 156;
-
-    private static final int DEPENDENT_LAYER_ID = 212;
-    private static final int DEPENDENT_LAYER_VERSION = 234;
-    private static final int DEPENDENT_LAYER_SUBTYPE = 256;
-
-    private static final int DIFFERENT_LAYER_ID = 99;
-    private static final int DIFFERENT_PUBLISHER_ID = 2;
-
-    private static final VmsLayer VMS_LAYER = new VmsLayer(
-            LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-    private static final VmsLayer DEPENDENT_VMS_LAYER = new VmsLayer(
-            DEPENDENT_LAYER_ID,
-            DEPENDENT_LAYER_SUBTYPE,
-            DEPENDENT_LAYER_VERSION);
-
-    private static final VmsLayer ANOTHER_DEPENDENT_VMS_LAYER = new VmsLayer(
-            DIFFERENT_LAYER_ID,
-            DEPENDENT_LAYER_SUBTYPE,
-            DEPENDENT_LAYER_VERSION);
-
-    private static final VmsLayerDependency VMS_LAYER_DEPENDENCY =
-            new VmsLayerDependency(
-                    VMS_LAYER,
-                    new HashSet<>(Arrays.asList(DEPENDENT_VMS_LAYER)));
-
-    private static final VmsLayerDependency ANOTHER_VMS_LAYER_DEPENDENCY =
-            new VmsLayerDependency(
-                    VMS_LAYER,
-                    new HashSet<>(Arrays.asList(ANOTHER_DEPENDENT_VMS_LAYER)));
-
-    public void testNoOfferingParcel() throws Exception {
-        VmsLayersOffering vmsLayersOffering =
-                new VmsLayersOffering(new HashSet<>(Arrays.asList()), PUBLISHER_ID);
-
-        Parcel parcel = Parcel.obtain();
-        vmsLayersOffering.writeToParcel(parcel, vmsLayersOffering.describeContents());
-        parcel.setDataPosition(0);
-        VmsLayersOffering createdFromParcel = VmsLayersOffering.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsLayersOffering, createdFromParcel);
-    }
-
-    public void testMultipleOfferingParcel() throws Exception {
-        VmsLayersOffering vmsLayersOffering =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList(
-                                VMS_LAYER_DEPENDENCY,
-                                ANOTHER_VMS_LAYER_DEPENDENCY)),
-                        PUBLISHER_ID);
-
-        Parcel parcel = Parcel.obtain();
-        vmsLayersOffering.writeToParcel(parcel, vmsLayersOffering.describeContents());
-        parcel.setDataPosition(0);
-        VmsLayersOffering createdFromParcel = VmsLayersOffering.CREATOR.createFromParcel(parcel);
-
-        assertEquals(vmsLayersOffering, createdFromParcel);
-    }
-
-    public void testNoOfferingEquality() throws Exception {
-        VmsLayersOffering original =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList()), PUBLISHER_ID);
-
-        VmsLayersOffering similar =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList()), PUBLISHER_ID);
-
-        assertEquals(original.getDependencies(), similar.getDependencies());
-        assertEquals(original.getPublisherId(), similar.getPublisherId());
-        assertEquals(original, similar);
-    }
-
-    public void testMultipleOfferingEquality() throws Exception {
-        VmsLayersOffering original =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList(VMS_LAYER_DEPENDENCY, ANOTHER_VMS_LAYER_DEPENDENCY)), PUBLISHER_ID);
-
-        VmsLayersOffering similar =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList(VMS_LAYER_DEPENDENCY, ANOTHER_VMS_LAYER_DEPENDENCY)), PUBLISHER_ID);
-
-        assertEquals(original.getDependencies(), similar.getDependencies());
-        assertEquals(original.getPublisherId(), similar.getPublisherId());
-        assertEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnPublisherId() throws Exception {
-        VmsLayersOffering original =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList(VMS_LAYER_DEPENDENCY)), PUBLISHER_ID);
-
-        VmsLayersOffering similar =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList(VMS_LAYER_DEPENDENCY)), DIFFERENT_PUBLISHER_ID);
-
-        assertNotEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnDependency() throws Exception {
-        VmsLayersOffering original =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList(VMS_LAYER_DEPENDENCY)), PUBLISHER_ID);
-
-        VmsLayersOffering similar =
-                new VmsLayersOffering(
-                        new HashSet<>(Arrays.asList(ANOTHER_VMS_LAYER_DEPENDENCY)), PUBLISHER_ID);
-
-        assertNotEquals(original, similar);
-    }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsPublishersInfoTest.java b/tests/carservice_unit_test/src/com/android/car/VmsPublishersInfoTest.java
deleted file mode 100644
index 1938a19..0000000
--- a/tests/carservice_unit_test/src/com/android/car/VmsPublishersInfoTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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 org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Before;
-import org.junit.Test;
-
-public class VmsPublishersInfoTest {
-    public static final byte[] MOCK_INFO_1 = new byte[]{2, 3, 5, 7, 11, 13, 17};
-    public static final byte[] SAME_MOCK_INFO_1 = new byte[]{2, 3, 5, 7, 11, 13, 17};
-    public static final byte[] MOCK_INFO_2 = new byte[]{2, 3, 5, 7, 11, 13, 17, 19};
-
-    private VmsPublishersInfo mVmsPublishersInfo;
-
-    @Before
-    public void setUp() throws Exception {
-        mVmsPublishersInfo = new VmsPublishersInfo();
-    }
-
-    @Test
-    public void testSingleInfo() throws Exception {
-        int id = mVmsPublishersInfo.getIdForInfo(MOCK_INFO_1);
-        assertEquals(1, id);
-        assertArrayEquals(MOCK_INFO_1, mVmsPublishersInfo.getPublisherInfo(id));
-    }
-
-    @Test
-    public void testSingleInfo_NoSuchId() throws Exception {
-        assertEquals(0, mVmsPublishersInfo.getPublisherInfo(12345).length);
-    }
-
-    @Test
-    public void testTwoInfos() throws Exception {
-        int id1 = mVmsPublishersInfo.getIdForInfo(MOCK_INFO_1);
-        int id2 = mVmsPublishersInfo.getIdForInfo(MOCK_INFO_2);
-        assertEquals(1, id1);
-        assertEquals(2, id2);
-        assertArrayEquals(MOCK_INFO_1, mVmsPublishersInfo.getPublisherInfo(id1));
-        assertArrayEquals(MOCK_INFO_2, mVmsPublishersInfo.getPublisherInfo(id2));
-    }
-
-    @Test
-    public void testSingleInfoInsertedTwice() throws Exception {
-        int id = mVmsPublishersInfo.getIdForInfo(MOCK_INFO_1);
-        assertEquals(1, id);
-
-        int sameId = mVmsPublishersInfo.getIdForInfo(SAME_MOCK_INFO_1);
-        assertEquals(sameId, id);
-    }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsSubscriptionStateTest.java b/tests/carservice_unit_test/src/com/android/car/VmsSubscriptionStateTest.java
deleted file mode 100644
index ea41286..0000000
--- a/tests/carservice_unit_test/src/com/android/car/VmsSubscriptionStateTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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 org.junit.Assert.assertNotEquals;
-
-import android.car.vms.VmsAssociatedLayer;
-import android.car.vms.VmsLayer;
-import android.car.vms.VmsSubscriptionState;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import android.os.Parcel;
-
-import java.util.Arrays;
-import java.util.HashSet;
-
-/*
- * A class to test the VmsSubscriptionState parcelability.
- */
-@SmallTest
-public class VmsSubscriptionStateTest extends AndroidTestCase {
-    private static final int LAYER_ID = 12;
-    private static final int LAYER_VERSION = 34;
-    private static final int LAYER_SUBTYPE = 56;
-
-    private static final int PUBLISHER_ID_1= 111;
-    private static final int PUBLISHER_ID_2= 222;
-
-    private static final int DIFFERENT_LAYER_ID = 99;
-
-    private static final int SEQUENCE_NUMBER = 1;
-    private static final int DIFFERENT_SEQUENCE_NUMBER = 2;
-
-    private static final VmsLayer VMS_LAYER = new VmsLayer(
-            LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-    private static final VmsLayer ANOTHER_VMS_LAYER = new VmsLayer(
-            DIFFERENT_LAYER_ID,
-            LAYER_SUBTYPE,
-            LAYER_VERSION);
-
-    private static final VmsAssociatedLayer VMS_ASSOCIATED_LAYER =
-            new VmsAssociatedLayer(
-                    VMS_LAYER,
-                    new HashSet<>(Arrays.asList(PUBLISHER_ID_1)));
-
-    public void testNoSubscriptionsParcel() throws Exception {
-        VmsSubscriptionState vmsSubscriptionState =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList()),
-                        new HashSet<>(Arrays.asList()));
-
-        Parcel parcel = Parcel.obtain();
-        vmsSubscriptionState.writeToParcel(parcel, vmsSubscriptionState.describeContents());
-        parcel.setDataPosition(0);
-        VmsSubscriptionState createdFromParcel = VmsSubscriptionState.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsSubscriptionState, createdFromParcel);
-    }
-
-    public void testMultipleSubscriptionsParcel() throws Exception {
-        VmsSubscriptionState vmsSubscriptionState =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList(VMS_LAYER)),
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)));
-
-        Parcel parcel = Parcel.obtain();
-        vmsSubscriptionState.writeToParcel(parcel, vmsSubscriptionState.describeContents());
-        parcel.setDataPosition(0);
-        VmsSubscriptionState createdFromParcel = VmsSubscriptionState.CREATOR.createFromParcel(parcel);
-        assertEquals(vmsSubscriptionState, createdFromParcel);
-    }
-
-    public void testNoSubscriptionsEquality() throws Exception {
-        VmsSubscriptionState original =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList()),
-                        new HashSet<>(Arrays.asList()));
-
-        VmsSubscriptionState similar =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList()),
-                        new HashSet<>(Arrays.asList()));
-
-        assertEquals(original.getSequenceNumber(), similar.getSequenceNumber());
-        assertEquals(original.getLayers(), similar.getLayers());
-        assertEquals(original.getAssociatedLayers(), similar.getAssociatedLayers());
-        assertEquals(original, similar);
-    }
-
-    public void testMultipleSubscriptionsEquality() throws Exception {
-        VmsSubscriptionState original =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList(VMS_LAYER)),
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)));
-
-        VmsSubscriptionState similar =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList(VMS_LAYER)),
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)));
-
-        assertEquals(original.getSequenceNumber(), similar.getSequenceNumber());
-        assertEquals(original.getLayers(), similar.getLayers());
-        assertEquals(original.getAssociatedLayers(), similar.getAssociatedLayers());
-        assertEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnSequenceNumber() throws Exception {
-        VmsSubscriptionState original =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList(VMS_LAYER)),
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)));
-
-        VmsSubscriptionState similar =
-                new VmsSubscriptionState(
-                        DIFFERENT_SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList(VMS_LAYER)),
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)));
-
-        if (!original.equals(similar)) {
-            return;
-        }
-        fail("VmsSubscriptionState with different sequence numbers appear to be equal. original: " +
-                original +
-                ", similar: " +
-                similar);
-    }
-
-    public void testVerifyNonEqualOnLayers() throws Exception {
-        VmsSubscriptionState original =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList(VMS_LAYER)),
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)));
-
-        VmsSubscriptionState similar =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList()),
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)));
-
-        assertNotEquals(original, similar);
-    }
-
-    public void testVerifyNonEqualOnAssociatedLayers() throws Exception {
-        VmsSubscriptionState original =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList(VMS_LAYER)),
-                        new HashSet<>(Arrays.asList(VMS_ASSOCIATED_LAYER)));
-
-        VmsSubscriptionState similar =
-                new VmsSubscriptionState(
-                        SEQUENCE_NUMBER,
-                        new HashSet<>(Arrays.asList(VMS_LAYER)),
-                        new HashSet<>(Arrays.asList()));
-
-        assertNotEquals(original, similar);
-    }
-}
\ No newline at end of file
diff --git a/tests/carservice_unit_test/src/com/android/car/com/android/car/cluster/InstrumentClusterServiceTest.java b/tests/carservice_unit_test/src/com/android/car/cluster/InstrumentClusterServiceTest.java
similarity index 96%
rename from tests/carservice_unit_test/src/com/android/car/com/android/car/cluster/InstrumentClusterServiceTest.java
rename to tests/carservice_unit_test/src/com/android/car/cluster/InstrumentClusterServiceTest.java
index f1bd536..79ebe4f 100644
--- a/tests/carservice_unit_test/src/com/android/car/com/android/car/cluster/InstrumentClusterServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/cluster/InstrumentClusterServiceTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 
+import android.car.CarAppFocusManager;
 import android.car.cluster.renderer.IInstrumentCluster;
 import android.car.cluster.renderer.IInstrumentClusterNavigation;
 import android.car.navigation.CarNavigationInstrumentCluster;
@@ -34,6 +35,7 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.os.Looper;
+import android.os.Process;
 import android.view.KeyEvent;
 
 import androidx.test.InstrumentationRegistry;
@@ -125,6 +127,9 @@
         if (connect) {
             notifyRendererServiceConnection();
         }
+        // Give nav focus to the test
+        mService.onFocusAcquired(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, Process.myUid(),
+                Process.myPid());
     }
 
     private void notifyRendererServiceConnection() {
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
index 033bb9a..56d5395 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
@@ -33,9 +33,11 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -60,12 +62,18 @@
 import android.hardware.automotive.vehicle.V2_0.UsersInfo;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.ServiceSpecificException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.Pair;
 
+import com.android.car.CarLocalServices;
+import com.android.car.user.CarUserService;
+
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -116,6 +124,10 @@
 
     @Mock
     private VehicleHal mVehicleHal;
+    @Mock
+    private CarUserService mCarUserService;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
 
     private final UserInfo mUser0 = new UserInfo();
     private final UserInfo mUser10 = new UserInfo();
@@ -127,7 +139,7 @@
 
     @Before
     public void setFixtures() {
-        mUserHalService = spy(new UserHalService(mVehicleHal));
+        mUserHalService = spy(new UserHalService(mVehicleHal, mHandler));
         // Needs at least one property, otherwise isSupported() will return false
         mUserHalService.takeProperties(Arrays.asList(newSubscribableConfig(INITIAL_USER_INFO)));
 
@@ -141,6 +153,13 @@
         mUsersInfo.existingUsers = new ArrayList<>(2);
         mUsersInfo.existingUsers.add(mUser0);
         mUsersInfo.existingUsers.add(mUser10);
+
+        CarLocalServices.addService(CarUserService.class, mCarUserService);
+    }
+
+    @After
+    public void clearFixtures() {
+        CarLocalServices.removeServiceForTest(CarUserService.class);
     }
 
     @Test
@@ -359,7 +378,7 @@
         VehiclePropValue propResponse = UserHalHelper.createPropRequest(INITIAL_USER_INFO,
                     REQUEST_ID_PLACE_HOLDER, InitialUserInfoResponseAction.CREATE);
         propResponse.value.int32Values.add(newUserFlags);
-        propResponse.value.stringValue = newUserName;
+        propResponse.value.stringValue = "||" + newUserName;
 
         AtomicReference<VehiclePropValue> reqCaptor = replySetPropertyWithOnChangeEvent(
                 INITIAL_USER_INFO, propResponse, /* rightRequestId= */ true);
@@ -378,6 +397,7 @@
         assertThat(callback.status).isEqualTo(HalCallback.STATUS_OK);
         InitialUserInfoResponse actualResponse = callback.response;
         assertThat(actualResponse.action).isEqualTo(InitialUserInfoResponseAction.CREATE);
+        assertThat(actualResponse.userLocales).isEmpty();
         assertThat(actualResponse.userNameToCreate).isEqualTo(newUserName);
         UserInfo newUser = actualResponse.userToSwitchOrCreate;
         assertThat(newUser).isNotNull();
@@ -464,7 +484,7 @@
     @Test
     public void testSwitchUser_halReturnedInvalidMessageType() throws Exception {
         VehiclePropValue propResponse = UserHalHelper.createPropRequest(SWITCH_USER,
-                    REQUEST_ID_PLACE_HOLDER, SwitchUserMessageType.VEHICLE_REQUEST);
+                REQUEST_ID_PLACE_HOLDER, SwitchUserMessageType.LEGACY_ANDROID_SWITCH);
         propResponse.value.int32Values.add(SwitchUserStatus.SUCCESS);
 
         AtomicReference<VehiclePropValue> reqCaptor = replySetPropertyWithOnChangeEvent(
@@ -576,6 +596,35 @@
     }
 
     @Test
+    public void testUserSwitch_OEMRequest_success() throws Exception {
+        int requestId = -4;
+        int targetUserId = 11;
+        VehiclePropValue propResponse = UserHalHelper.createPropRequest(SWITCH_USER,
+                requestId, SwitchUserMessageType.VEHICLE_REQUEST);
+
+        propResponse.value.int32Values.add(targetUserId);
+
+        mUserHalService.onHalEvents(Arrays.asList(propResponse));
+        waitForHandler();
+
+        verify(mCarUserService).switchAndroidUserFromHal(requestId, targetUserId);
+    }
+
+    @Test
+    public void testUserSwitch_OEMRequest_failure_positiveRequestId() throws Exception {
+        int requestId = 4;
+        int targetUserId = 11;
+        VehiclePropValue propResponse = UserHalHelper.createPropRequest(SWITCH_USER,
+                requestId, SwitchUserMessageType.VEHICLE_REQUEST);
+        propResponse.value.int32Values.add(targetUserId);
+
+        mUserHalService.onHalEvents(Arrays.asList(propResponse));
+        waitForHandler();
+
+        verify(mCarUserService, never()).switchAndroidUserFromHal(anyInt(), anyInt());
+    }
+
+    @Test
     public void testPostSwitchResponse_noUsersInfo() {
         assertThrows(NullPointerException.class,
                 () -> mUserHalService.postSwitchResponse(42, mUser10, null));
@@ -1052,6 +1101,13 @@
         return request;
     }
 
+    /**
+     * Run empty runnable to make sure that all posted handlers are done.
+     */
+    private void waitForHandler() {
+        mHandler.runWithScissors(() -> { }, /* Default timeout */ CALLBACK_TIMEOUT_TIMEOUT);
+    }
+
     private void mockNextRequestId(int requestId) {
         doReturn(requestId).when(mUserHalService).getNextRequestId();
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/hardware/power/CarPowerManagerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/hardware/power/CarPowerManagerUnitTest.java
new file mode 100644
index 0000000..6100dbc
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/hardware/power/CarPowerManagerUnitTest.java
@@ -0,0 +1,391 @@
+/*
+ * 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.
+ */
+
+package com.android.car.hardware.power;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.car.Car;
+import android.car.hardware.power.CarPowerManager;
+import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.car.test.mocks.JavaMockitoHelper;
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
+import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateShutdownParam;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.car.CarPowerManagementService;
+import com.android.car.MockedPowerHalService;
+import com.android.car.R;
+import com.android.car.hal.PowerHalService;
+import com.android.car.hal.PowerHalService.PowerState;
+import com.android.car.systeminterface.DisplayInterface;
+import com.android.car.systeminterface.SystemInterface;
+import com.android.car.systeminterface.SystemStateInterface;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.time.Duration;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
+
+@SmallTest
+public class CarPowerManagerUnitTest extends AbstractExtendedMockitoTestCase {
+    private static final String TAG = CarPowerManagerUnitTest.class.getSimpleName();
+    private static final long WAIT_TIMEOUT_MS = 5_000;
+    private static final long WAIT_TIMEOUT_LONG_MS = 10_000;
+    // A shorter value for use when the test is expected to time out
+    private static final long WAIT_WHEN_TIMEOUT_EXPECTED_MS = 100;
+
+    private final MockDisplayInterface mDisplayInterface = new MockDisplayInterface();
+    private final MockSystemStateInterface mSystemStateInterface = new MockSystemStateInterface();
+    private final Context mContext =
+            InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+    private MockedPowerHalService mPowerHal;
+    private SystemInterface mSystemInterface;
+    private CarPowerManagementService mService;
+    private CarPowerManager mCarPowerManager;
+
+    @Mock
+    private Resources mResources;
+    @Mock
+    private Car mCar;
+
+    @Before
+    public void setUp() throws Exception {
+        mPowerHal = new MockedPowerHalService(true /*isPowerStateSupported*/,
+                true /*isDeepSleepAllowed*/, true /*isTimedWakeupAllowed*/);
+        mSystemInterface = SystemInterface.Builder.defaultSystemInterface(mContext)
+            .withDisplayInterface(mDisplayInterface)
+            .withSystemStateInterface(mSystemStateInterface)
+            .build();
+        setService();
+        mCarPowerManager = new CarPowerManager(mCar, mService);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mService != null) {
+            mService.release();
+        }
+    }
+
+    @Test
+    public void testRequestShutdownOnNextSuspend_positive() throws Exception {
+        setPowerOn();
+        // Tell it to shutdown
+        mCarPowerManager.requestShutdownOnNextSuspend();
+        // Request suspend
+        setPowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                VehicleApPowerStateShutdownParam.CAN_SLEEP);
+        // Verify shutdown
+        assertStateReceivedForShutdownOrSleepWithPostpone(PowerHalService.SET_SHUTDOWN_START, 0);
+    }
+
+    @Test
+    public void testRequestShutdownOnNextSuspend_negative() throws Exception {
+        setPowerOn();
+
+        // Do not tell it to shutdown
+
+        // Request suspend
+        setPowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                VehicleApPowerStateShutdownParam.CAN_SLEEP);
+        // Verify suspend
+        assertStateReceivedForShutdownOrSleepWithPostpone(PowerHalService.SET_DEEP_SLEEP_ENTRY, 0);
+    }
+
+    @Test
+    public void testScheduleNextWakeupTime() throws Exception {
+        setPowerOn();
+
+        int wakeTime = 1234;
+        mCarPowerManager.scheduleNextWakeupTime(wakeTime);
+
+        setPowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                VehicleApPowerStateShutdownParam.CAN_SLEEP);
+
+        // Verify that we suspended with the requested wake-up time
+        assertStateReceivedForShutdownOrSleepWithPostpone(
+                PowerHalService.SET_DEEP_SLEEP_ENTRY, wakeTime);
+    }
+
+    @Test
+    public void testSetListener() throws Exception {
+        setPowerOn();
+
+        WaitablePowerStateListener listener = new WaitablePowerStateListener(2);
+
+        setPowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                VehicleApPowerStateShutdownParam.CAN_SLEEP);
+
+        assertStateReceivedForShutdownOrSleepWithPostpone(PowerHalService.SET_DEEP_SLEEP_ENTRY, 0);
+
+        int finalState = listener.await();
+        assertThat(finalState).isEqualTo(PowerHalService.SET_DEEP_SLEEP_ENTRY);
+    }
+
+    @Test
+    public void testSetListenerWithCompletion() throws Exception {
+        setPowerOn();
+
+        WaitablePowerStateListenerWithCompletion listener =
+                new WaitablePowerStateListenerWithCompletion(2);
+
+        setPowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                VehicleApPowerStateShutdownParam.CAN_SLEEP);
+        assertStateReceivedForShutdownOrSleepWithPostpone(PowerHalService.SET_DEEP_SLEEP_ENTRY, 0);
+
+        int finalState = listener.await();
+        assertThat(finalState).isEqualTo(PowerHalService.SET_DEEP_SLEEP_ENTRY);
+    }
+
+    @Test
+    public void testClearListener() throws Exception {
+        setPowerOn();
+
+        // Set a listener with a short timeout, because we expect the timeout to happen
+        WaitablePowerStateListener listener =
+                new WaitablePowerStateListener(1, WAIT_WHEN_TIMEOUT_EXPECTED_MS);
+
+        mCarPowerManager.clearListener();
+
+        setPowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                VehicleApPowerStateShutdownParam.CAN_SLEEP);
+
+        assertStateReceivedForShutdownOrSleepWithPostpone(PowerHalService.SET_DEEP_SLEEP_ENTRY, 0);
+        // Verify that the listener didn't run
+        assertThrows(IllegalStateException.class, () -> listener.await());
+    }
+
+    @Test
+    public void testGetPowerState() throws Exception {
+        setPowerOn();
+        assertThat(mCarPowerManager.getPowerState()).isEqualTo(PowerHalService.SET_ON);
+
+        // Request suspend
+        setPowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+                        VehicleApPowerStateShutdownParam.CAN_SLEEP);
+        assertStateReceivedForShutdownOrSleepWithPostpone(PowerHalService.SET_DEEP_SLEEP_ENTRY, 0);
+        assertThat(mCarPowerManager.getPowerState())
+                .isEqualTo(PowerHalService.SET_DEEP_SLEEP_ENTRY);
+    }
+
+    /**
+     * Helper method to create mService and initialize a test case
+     */
+    private void setService() throws Exception {
+        Log.i(TAG, "setService(): overridden overlay properties: "
+                + "config_disableUserSwitchDuringResume="
+                + mResources.getBoolean(R.bool.config_disableUserSwitchDuringResume)
+                + ", maxGarageModeRunningDurationInSecs="
+                + mResources.getInteger(R.integer.maxGarageModeRunningDurationInSecs));
+        mService = new CarPowerManagementService(mContext, mResources, mPowerHal,
+                mSystemInterface, null, null, null);
+        mService.init();
+        mService.setShutdownTimersForTest(0, 0);
+        assertStateReceived(MockedPowerHalService.SET_WAIT_FOR_VHAL, 0);
+    }
+
+    private void assertStateReceived(int expectedState, int expectedParam) throws Exception {
+        int[] state = mPowerHal.waitForSend(WAIT_TIMEOUT_MS);
+        assertThat(state).asList().containsExactly(expectedState, expectedParam).inOrder();
+    }
+
+    /**
+     * Helper method to get the system into ON
+     */
+    private void setPowerOn() throws Exception {
+        setPowerState(VehicleApPowerStateReq.ON, 0);
+        assertThat(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS)).isTrue();
+    }
+
+    /**
+     * Helper to set the PowerHal state
+     *
+     * @param stateEnum Requested state enum
+     * @param stateParam Addition state parameter
+     */
+    private void setPowerState(int stateEnum, int stateParam) {
+        mPowerHal.setCurrentPowerState(new PowerState(stateEnum, stateParam));
+    }
+
+    private void assertStateReceivedForShutdownOrSleepWithPostpone(
+            int lastState, int stateParameter) throws Exception {
+        long startTime = System.currentTimeMillis();
+        while (true) {
+            int[] state = mPowerHal.waitForSend(WAIT_TIMEOUT_LONG_MS);
+            if (state[0] == lastState) {
+                assertThat(state[1]).isEqualTo(stateParameter);
+                return;
+            }
+            assertThat(state[0]).isEqualTo(PowerHalService.SET_SHUTDOWN_POSTPONE);
+            assertThat(System.currentTimeMillis() - startTime).isLessThan(WAIT_TIMEOUT_LONG_MS);
+        }
+    }
+
+    private static final class MockDisplayInterface implements DisplayInterface {
+        private boolean mDisplayOn = true;
+        private final Semaphore mDisplayStateWait = new Semaphore(0);
+
+        @Override
+        public void setDisplayBrightness(int brightness) {}
+
+        @Override
+        public synchronized void setDisplayState(boolean on) {
+            mDisplayOn = on;
+            mDisplayStateWait.release();
+        }
+
+        public synchronized boolean getDisplayState() {
+            return mDisplayOn;
+        }
+
+        public boolean waitForDisplayStateChange(long timeoutMs) throws Exception {
+            JavaMockitoHelper.await(mDisplayStateWait, timeoutMs);
+            return mDisplayOn;
+        }
+
+        @Override
+        public void startDisplayStateMonitoring(CarPowerManagementService service) {}
+
+        @Override
+        public void stopDisplayStateMonitoring() {}
+
+        @Override
+        public void refreshDisplayBrightness() {}
+    }
+
+    /**
+     * Helper class to set a power-state listener,
+     * verify that the listener gets called the
+     * right number of times, and return the final
+     * power state.
+     */
+    private final class WaitablePowerStateListener {
+        private final CountDownLatch mLatch;
+        private int mListenedState = -1;
+        private long mTimeoutValue = WAIT_TIMEOUT_MS;
+
+        WaitablePowerStateListener(int initialCount, long customTimeout) {
+            this(initialCount);
+            mTimeoutValue = customTimeout;
+        }
+
+        WaitablePowerStateListener(int initialCount) {
+            mLatch = new CountDownLatch(initialCount);
+            mCarPowerManager.setListener(
+                    (state) -> {
+                        mListenedState = state;
+                        mLatch.countDown();
+                    });
+        }
+
+        int await() throws Exception {
+            JavaMockitoHelper.await(mLatch, WAIT_TIMEOUT_MS);
+            return mListenedState;
+        }
+    }
+
+    /**
+     * Helper class to set a power-state listener with completion,
+     * verify that the listener gets called the right number of times,
+     * verify that the CompletableFuture is provided, complete the
+     * CompletableFuture, and return the final power state.
+     */
+    private final class WaitablePowerStateListenerWithCompletion {
+        private final CountDownLatch mLatch;
+        private int mListenedState = -1;
+        WaitablePowerStateListenerWithCompletion(int initialCount) {
+            mLatch = new CountDownLatch(initialCount);
+            mCarPowerManager.setListenerWithCompletion(
+                    (state, future) -> {
+                        mListenedState = state;
+                        if (state == PowerHalService.SET_SHUTDOWN_PREPARE) {
+                            assertThat(future).isNotNull();
+                            future.complete(null);
+                        } else {
+                            assertThat(future).isNull();
+                        }
+                        mLatch.countDown();
+                    });
+        }
+
+        int await() throws Exception {
+            JavaMockitoHelper.await(mLatch, WAIT_TIMEOUT_MS);
+            return mListenedState;
+        }
+    }
+
+    private static final class MockSystemStateInterface implements SystemStateInterface {
+        private final Semaphore mShutdownWait = new Semaphore(0);
+        private final Semaphore mSleepWait = new Semaphore(0);
+        private final Semaphore mSleepExitWait = new Semaphore(0);
+        private boolean mWakeupCausedByTimer = false;
+
+        @Override
+        public void shutdown() {
+            mShutdownWait.release();
+        }
+
+        public void waitForShutdown(long timeoutMs) throws Exception {
+            JavaMockitoHelper.await(mShutdownWait, timeoutMs);
+        }
+
+        @Override
+        public boolean enterDeepSleep() {
+            mSleepWait.release();
+            try {
+                mSleepExitWait.acquire();
+            } catch (InterruptedException e) {
+            }
+            return true;
+        }
+
+        public void waitForSleepEntryAndWakeup(long timeoutMs) throws Exception {
+            JavaMockitoHelper.await(mSleepWait, timeoutMs);
+            mSleepExitWait.release();
+        }
+
+        @Override
+        public void scheduleActionForBootCompleted(Runnable action, Duration delay) {}
+
+        @Override
+        public boolean isWakeupCausedByTimer() {
+            Log.i(TAG, "isWakeupCausedByTimer:" + mWakeupCausedByTimer);
+            return mWakeupCausedByTimer;
+        }
+
+        public synchronized void setWakeupCausedByTimer(boolean set) {
+            mWakeupCausedByTimer = set;
+        }
+
+        @Override
+        public boolean isSystemSupportingDeepSleep() {
+            return true;
+        }
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/pm/VendorServiceControllerTest.java b/tests/carservice_unit_test/src/com/android/car/pm/VendorServiceControllerTest.java
index 3be64a5..ddea0c0 100644
--- a/tests/carservice_unit_test/src/com/android/car/pm/VendorServiceControllerTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/pm/VendorServiceControllerTest.java
@@ -214,13 +214,13 @@
             @UserIdInt int userId) throws InterruptedException {
         // Adding a blocking listener to ensure CarUserService event notification is completed
         // before proceeding with test execution.
-        BlockingUserLifecycleListener blockingListener = BlockingUserLifecycleListener
-                .newDefaultListener();
+        BlockingUserLifecycleListener blockingListener =
+                BlockingUserLifecycleListener.forAnyEvent().build();
         mCarUserService.addUserLifecycleListener(blockingListener);
 
         runOnMainThreadAndWaitForIdle(() -> mCarUserService.onUserLifecycleEvent(eventType,
                 /* timestampMs= */ 0, /* fromUserId= */ UserHandle.USER_NULL, userId));
-        blockingListener.waitForEvent();
+        blockingListener.waitForAnyEvent();
     }
 
     /** Overrides framework behavior to succeed on binding/starting processes. */
diff --git a/tests/carservice_unit_test/src/com/android/car/testapi/BlockingUserLifecycleListenerTest.java b/tests/carservice_unit_test/src/com/android/car/testapi/BlockingUserLifecycleListenerTest.java
new file mode 100644
index 0000000..d236848
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/testapi/BlockingUserLifecycleListenerTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.
+ */
+
+package com.android.car.testapi;
+
+import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STARTING;
+import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPED;
+import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STOPPING;
+import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
+import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
+import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.testng.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.car.testapi.BlockingUserLifecycleListener;
+import android.car.user.CarUserManager;
+import android.car.user.CarUserManager.UserLifecycleEvent;
+import android.car.user.CarUserManager.UserLifecycleEventType;
+import android.os.UserHandle;
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public final class BlockingUserLifecycleListenerTest {
+
+    private static final String TAG = BlockingUserLifecycleListenerTest.class.getSimpleName();
+
+    @Test
+    public void testListener_forAnyEvent_invalidBuilderMethods() throws Exception {
+        BlockingUserLifecycleListener.Builder builder = BlockingUserLifecycleListener.forAnyEvent()
+                .setTimeout(666);
+
+        assertThrows(IllegalStateException.class, () -> builder.forUser(10));
+        assertThrows(IllegalStateException.class, () -> builder.forPreviousUser(10));
+        assertThrows(IllegalStateException.class,
+                () -> builder.addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPED));
+    }
+
+    @Test
+    public void testForAnyEvent_invalidMethods() throws Exception {
+        BlockingUserLifecycleListener listener =  BlockingUserLifecycleListener.forAnyEvent()
+                .build();
+
+        assertThrows(IllegalStateException.class, () -> listener.waitForEvents());
+        assertThrows(IllegalStateException.class, () -> listener.getAllReceivedEvents());
+    }
+
+    @Test
+    public void testForAnyEvent_timesout() throws Exception {
+        BlockingUserLifecycleListener listener =  BlockingUserLifecycleListener.forAnyEvent()
+                .build();
+
+        assertThrows(IllegalStateException.class, () -> listener.waitForAnyEvent());
+    }
+
+    @Test
+    public void testForAnyEvent_ok() throws Exception {
+        BlockingUserLifecycleListener listener =  BlockingUserLifecycleListener.forAnyEvent()
+                .build();
+        sendAsyncEvents(listener, 10, USER_LIFECYCLE_EVENT_TYPE_UNLOCKED);
+
+        UserLifecycleEvent event = listener.waitForAnyEvent();
+        assertEvent(event, 10, USER_LIFECYCLE_EVENT_TYPE_UNLOCKED);
+    }
+
+    @Test
+    public void testForSpecificEvents_invalidMethods() throws Exception {
+        BlockingUserLifecycleListener listener =  BlockingUserLifecycleListener.forSpecificEvents()
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING).build();
+
+        assertThrows(IllegalStateException.class, () -> listener.waitForAnyEvent());
+    }
+
+    @Test
+    public void testForSpecificEvents_receivedOnlyExpected() throws Exception {
+        BlockingUserLifecycleListener listener =  BlockingUserLifecycleListener.forSpecificEvents()
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)
+                .build();
+
+        sendAsyncEvents(listener, 10,
+                USER_LIFECYCLE_EVENT_TYPE_STARTING,
+                USER_LIFECYCLE_EVENT_TYPE_UNLOCKED);
+
+        List<UserLifecycleEvent> events = listener.waitForEvents();
+        assertEvents(events, 10,
+                USER_LIFECYCLE_EVENT_TYPE_STARTING,
+                USER_LIFECYCLE_EVENT_TYPE_UNLOCKED);
+
+        List<UserLifecycleEvent> allReceivedEvents = listener.getAllReceivedEvents();
+        assertThat(allReceivedEvents).containsAllIn(events).inOrder();
+    }
+
+    @Test
+    public void testForSpecificEvents_receivedExtraEvents() throws Exception {
+        BlockingUserLifecycleListener listener =  BlockingUserLifecycleListener.forSpecificEvents()
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKING)
+                .build();
+
+        sendAsyncEvents(listener, 10,
+                USER_LIFECYCLE_EVENT_TYPE_STARTING,
+                USER_LIFECYCLE_EVENT_TYPE_SWITCHING,
+                USER_LIFECYCLE_EVENT_TYPE_UNLOCKING,
+                USER_LIFECYCLE_EVENT_TYPE_UNLOCKED);
+
+        List<UserLifecycleEvent> events = listener.waitForEvents();
+        assertEvents(events, 10,
+                USER_LIFECYCLE_EVENT_TYPE_STARTING,
+                USER_LIFECYCLE_EVENT_TYPE_UNLOCKING);
+
+        List<UserLifecycleEvent> allReceivedEvents = listener.getAllReceivedEvents();
+        assertEvents(allReceivedEvents, 10,
+                USER_LIFECYCLE_EVENT_TYPE_STARTING,
+                USER_LIFECYCLE_EVENT_TYPE_SWITCHING,
+                USER_LIFECYCLE_EVENT_TYPE_UNLOCKING,
+                USER_LIFECYCLE_EVENT_TYPE_UNLOCKED);
+    }
+
+    @Test
+    public void testForSpecificEvents_filterByUser() throws Exception {
+        BlockingUserLifecycleListener listener =  BlockingUserLifecycleListener.forSpecificEvents()
+                .forUser(10)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)
+                .build();
+        UserLifecycleEvent wrong1 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPING, 11);
+        UserLifecycleEvent wrong2 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPED, 11);
+        UserLifecycleEvent right1 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING, 10);
+        UserLifecycleEvent right2 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, 10);
+
+        sendAsyncEvents(listener, Arrays.asList(wrong1, right1, right2, wrong2));
+
+        List<UserLifecycleEvent> events = listener.waitForEvents();
+        assertThat(events).containsExactly(right1, right2).inOrder();
+
+        List<UserLifecycleEvent> allReceivedEvents = listener.getAllReceivedEvents();
+        assertThat(allReceivedEvents)
+                .containsExactly(wrong1, right1, right2, wrong2)
+                .inOrder();
+    }
+
+    @Test
+    public void testForSpecificEvents_filterByUserDuplicatedEventTypes() throws Exception {
+        BlockingUserLifecycleListener listener =  BlockingUserLifecycleListener.forSpecificEvents()
+                .forUser(10)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+                .build();
+        UserLifecycleEvent wrong1 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPING, 11);
+        UserLifecycleEvent wrong2 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPED, 11);
+        UserLifecycleEvent wrong3 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING, 11);
+        UserLifecycleEvent right1 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING, 10);
+        UserLifecycleEvent right2 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, 10);
+        UserLifecycleEvent right3 = new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING, 10);
+
+        sendAsyncEvents(listener, Arrays.asList(wrong1, right1, wrong2, right2, right3, wrong3));
+
+        List<UserLifecycleEvent> events = listener.waitForEvents();
+        assertThat(events).containsExactly(right1, right2, right3).inOrder();
+
+        List<UserLifecycleEvent> allReceivedEvents = listener.getAllReceivedEvents();
+        assertThat(allReceivedEvents)
+                .containsExactly(wrong1, right1, wrong2, right2, right3, wrong3)
+                .inOrder();
+    }
+
+    @Test
+    public void testForSpecificEvents_filterByPreviousUser() throws Exception {
+        BlockingUserLifecycleListener listener = BlockingUserLifecycleListener.forSpecificEvents()
+                .forPreviousUser(10)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)
+                .build();
+        UserLifecycleEvent wrong1 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPING, 11);
+        UserLifecycleEvent wrong2 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPED, 10);
+        UserLifecycleEvent wrong3 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING, 11, 10);
+        UserLifecycleEvent right1 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING, 10, 11);
+        UserLifecycleEvent right2 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, 10, 12);
+
+        sendAsyncEvents(listener, Arrays.asList(wrong1, right1, wrong2, right2, wrong3));
+
+        List<UserLifecycleEvent> events = listener.waitForEvents();
+        assertThat(events).containsExactly(right1, right2).inOrder();
+
+        List<UserLifecycleEvent> allReceivedEvents = listener.getAllReceivedEvents();
+        assertThat(allReceivedEvents)
+                .containsExactly(wrong1, right1, wrong2, right2, wrong3)
+                .inOrder();
+    }
+
+    @Test
+    public void testForSpecificEvents_filterByPreviousAndTargetUsers() throws Exception {
+        BlockingUserLifecycleListener listener = BlockingUserLifecycleListener.forSpecificEvents()
+                .forPreviousUser(10)
+                .forUser(11)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING)
+                .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)
+                .build();
+        UserLifecycleEvent wrong1 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPING, 11);
+        UserLifecycleEvent wrong2 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_STOPPED, 10);
+        UserLifecycleEvent wrong3 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING, 10, 12);
+        UserLifecycleEvent right1 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING, 10, 11);
+        UserLifecycleEvent right2 =
+                new UserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_UNLOCKED, 10, 11);
+
+        sendAsyncEvents(listener, Arrays.asList(wrong1, right1, wrong2, right2, wrong3));
+
+        List<UserLifecycleEvent> events = listener.waitForEvents();
+        assertThat(events).containsExactly(right1, right2).inOrder();
+
+        List<UserLifecycleEvent> allReceivedEvents = listener.getAllReceivedEvents();
+        assertThat(allReceivedEvents)
+                .containsExactly(wrong1, right1, wrong2, right2, wrong3)
+                .inOrder();
+    }
+
+    private static void sendAsyncEvents(@NonNull BlockingUserLifecycleListener listener,
+            @UserIdInt int userId, @UserLifecycleEventType int... eventTypes) {
+        List<UserLifecycleEvent> events = Arrays.stream(eventTypes)
+                .mapToObj((type) -> new UserLifecycleEvent(type, userId))
+                .collect(Collectors.toList());
+        sendAsyncEvents(listener, events);
+    }
+
+    private static void sendAsyncEvents(@NonNull BlockingUserLifecycleListener listener,
+            @NonNull List<UserLifecycleEvent> events) {
+        Log.d(TAG, "sendAsyncEvents(): " + events);
+        new Thread(() -> events.forEach((e) -> listener.onEvent(e)), "AsyncEventsThread").start();
+    }
+
+    private static void assertEvent(UserLifecycleEvent event, @UserIdInt int expectedUserId,
+            @UserLifecycleEventType int expectedType) {
+        assertThat(event).isNotNull();
+        assertWithMessage("wrong type on %s; expected %s", event,
+                CarUserManager.lifecycleEventTypeToString(expectedType)).that(event.getEventType())
+                        .isEqualTo(expectedType);
+        assertThat(event.getUserId()).isEqualTo(expectedUserId);
+        assertThat(event.getUserHandle().getIdentifier()).isEqualTo(expectedUserId);
+        assertThat(event.getPreviousUserId()).isEqualTo(UserHandle.USER_NULL);
+        assertThat(event.getPreviousUserHandle()).isNull();
+    }
+
+    private static void assertEvents(List<UserLifecycleEvent> events, @UserIdInt int expectedUserId,
+            @UserLifecycleEventType int... expectedTypes) {
+        assertThat(events).isNotNull();
+        assertThat(events).hasSize(expectedTypes.length);
+        for (int i = 0; i < expectedTypes.length; i++) {
+            int expectedType = expectedTypes[i];
+            UserLifecycleEvent event = events.get(i);
+            assertEvent(event, expectedUserId, expectedType);
+        }
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java
index abfd48d..13cebbd 100644
--- a/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.car.user;
 
+import static android.car.test.mocks.AndroidMockitoHelper.getResult;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUsers;
 import static android.car.test.util.UserTestingHelper.newUsers;
 import static android.car.testapi.CarMockitoHelper.mockHandleRemoteExceptionFromCarServiceWithDefaultValue;
@@ -26,21 +27,22 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.car.Car;
 import android.car.ICarUserService;
 import android.car.test.mocks.AbstractExtendedMockitoTestCase;
 import android.car.user.CarUserManager;
+import android.car.user.CarUserManager.UserLifecycleListener;
 import android.car.user.CarUserManager.UserSwitchUiCallback;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
 import android.car.user.UserSwitchResult;
 import android.content.pm.UserInfo;
 import android.os.RemoteException;
@@ -53,13 +55,9 @@
 import org.mockito.Mock;
 
 import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 
 public final class CarUserManagerUnitTest extends AbstractExtendedMockitoTestCase {
 
-    private static final long ASYNC_TIMEOUT_MS = 500;
-
     @Mock
     private Car mCar;
     @Mock
@@ -117,9 +115,55 @@
     }
 
     @Test
+    public void testAddListener_nullExecutor() {
+        assertThrows(NullPointerException.class, () -> mMgr.addListener(null, (e) -> { }));
+    }
+
+    @Test
+    public void testAddListener_nullListener() {
+        assertThrows(NullPointerException.class, () -> mMgr.addListener(Runnable::run, null));
+    }
+
+    @Test
+    public void testAddListener_sameListenerAddedTwice() {
+        UserLifecycleListener listener = (e) -> { };
+
+        mMgr.addListener(Runnable::run, listener);
+        assertThrows(IllegalStateException.class, () -> mMgr.addListener(Runnable::run, listener));
+    }
+
+    @Test
+    public void testAddListener_differentListenersAddedTwice() {
+        mMgr.addListener(Runnable::run, (e) -> { });
+        mMgr.addListener(Runnable::run, (e) -> { });
+    }
+
+    @Test
+    public void testRemoveListener_nullListener() {
+        assertThrows(NullPointerException.class, () -> mMgr.removeListener(null));
+    }
+
+    @Test
+    public void testRemoveListener_notAddedBefore() {
+        UserLifecycleListener listener = (e) -> { };
+
+        assertThrows(IllegalStateException.class, () -> mMgr.removeListener(listener));
+    }
+
+    @Test
+    public void testRemoveListener_addAndRemove() {
+        UserLifecycleListener listener = (e) -> { };
+
+        mMgr.addListener(Runnable::run, listener);
+        mMgr.removeListener(listener);
+
+        // Make sure it was removed
+        assertThrows(IllegalStateException.class, () -> mMgr.removeListener(listener));
+    }
+
+    @Test
     public void testSwitchUser_success() throws Exception {
-        expectServiceSwitchUserSucceeds(11, UserSwitchResult.STATUS_SUCCESSFUL,
-                "D'OH!");
+        expectServiceSwitchUserSucceeds(11, UserSwitchResult.STATUS_SUCCESSFUL, "D'OH!");
 
         AndroidFuture<UserSwitchResult> future = mMgr.switchUser(11);
 
@@ -170,24 +214,101 @@
 
     @Test
     public void testGetUserIdentificationAssociation_remoteException() throws Exception {
+        int[] types = new int[] {1};
+        when(mService.getUserIdentificationAssociation(types))
+                .thenThrow(new RemoteException("D'OH!"));
         mockHandleRemoteExceptionFromCarServiceWithDefaultValue(mCar);
-        assertThrows(IllegalArgumentException.class,
-                () -> mMgr.getUserIdentificationAssociation(new int[] {}));
+
+        assertThat(mMgr.getUserIdentificationAssociation(types)).isNull();
     }
 
     @Test
     public void testGetUserIdentificationAssociation_ok() throws Exception {
         int[] types = new int[] { 4, 8, 15, 16, 23, 42 };
-        GetUserIdentificationAssociationResponse expectedResponse =
-                new GetUserIdentificationAssociationResponse(null, new int[] {});
+        UserIdentificationAssociationResponse expectedResponse =
+                UserIdentificationAssociationResponse.forSuccess(types);
         when(mService.getUserIdentificationAssociation(types)).thenReturn(expectedResponse);
 
-        GetUserIdentificationAssociationResponse actualResponse =
+        UserIdentificationAssociationResponse actualResponse =
                 mMgr.getUserIdentificationAssociation(types);
 
         assertThat(actualResponse).isSameAs(expectedResponse);
     }
 
+    @Test
+    public void testSetUserIdentificationAssociation_nullTypes() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> mMgr.setUserIdentificationAssociation(null, new int[] {42}));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_emptyTypes() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> mMgr.setUserIdentificationAssociation(new int[0], new int[] {42}));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_nullValues() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> mMgr.setUserIdentificationAssociation(new int[] {42}, null));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_emptyValues() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> mMgr.setUserIdentificationAssociation(new int[] {42}, new int[0]));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_sizeMismatch() throws Exception {
+        assertThrows(IllegalArgumentException.class,
+                () -> mMgr.setUserIdentificationAssociation(new int[] {1}, new int[] {2, 3}));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_remoteException() throws Exception {
+        int[] types = new int[] {1};
+        int[] values = new int[] {2};
+        doThrow(new RemoteException("D'OH!")).when(mService)
+                .setUserIdentificationAssociation(anyInt(), same(types), same(values), notNull());
+        mockHandleRemoteExceptionFromCarServiceWithDefaultValue(mCar);
+
+        AndroidFuture<UserIdentificationAssociationResponse> future =
+                mMgr.setUserIdentificationAssociation(types, values);
+
+        assertThat(future).isNotNull();
+        UserIdentificationAssociationResponse result = getResult(future);
+        assertThat(result.isSuccess()).isFalse();
+        assertThat(result.getValues()).isNull();
+        assertThat(result.getErrorMessage()).isNull();
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_ok() throws Exception {
+        int[] types = new int[] { 1, 2, 3 };
+        int[] values = new int[] { 10, 20, 30 };
+        doAnswer((inv) -> {
+            @SuppressWarnings("unchecked")
+            AndroidFuture<UserIdentificationAssociationResponse> future =
+                    (AndroidFuture<UserIdentificationAssociationResponse>) inv.getArguments()[3];
+            UserIdentificationAssociationResponse response =
+                    UserIdentificationAssociationResponse.forSuccess(values, "D'OH!");
+            future.complete(response);
+            return null;
+        }).when(mService)
+                .setUserIdentificationAssociation(anyInt(), same(types), same(values), notNull());
+        mockHandleRemoteExceptionFromCarServiceWithDefaultValue(mCar);
+
+        AndroidFuture<UserIdentificationAssociationResponse> future =
+                mMgr.setUserIdentificationAssociation(types, values);
+
+        assertThat(future).isNotNull();
+        UserIdentificationAssociationResponse result = getResult(future);
+        assertThat(result.isSuccess()).isTrue();
+        assertThat(result.getValues()).asList().containsAllOf(10, 20, 30).inOrder();
+        assertThat(result.getErrorMessage()).isEqualTo("D'OH!");
+    }
+
     private void expectServiceSwitchUserSucceeds(@UserIdInt int userId,
             @UserSwitchResult.Status int status, @Nullable String errorMessage)
             throws RemoteException {
@@ -205,15 +326,6 @@
             .switchUser(eq(userId), anyInt(), notNull());
     }
 
-    @NonNull
-    private static <T> T getResult(@NonNull AndroidFuture<T> future) throws Exception {
-        try {
-            return future.get(ASYNC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        } catch (TimeoutException e) {
-            throw new IllegalStateException("not called in " + ASYNC_TIMEOUT_MS + "ms", e);
-        }
-    }
-
     private void setExistingUsers(int... userIds) {
         List<UserInfo> users = newUsers(userIds);
         mockUmGetUsers(mUserManager, users);
diff --git a/tests/carservice_unit_test/src/com/android/car/user/CarUserNoticeServiceTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserNoticeServiceTest.java
index 3b95c82..f50ad32 100644
--- a/tests/carservice_unit_test/src/com/android/car/user/CarUserNoticeServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/user/CarUserNoticeServiceTest.java
@@ -16,26 +16,25 @@
 
 package com.android.car.user;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static android.car.test.mocks.AndroidMockitoHelper.mockContextGetService;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
-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.app.AppOpsManager;
+import android.car.AbstractExtendedMockitoCarServiceTestCase;
 import android.car.hardware.power.CarPowerManager;
 import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
 import android.car.settings.CarSettings;
-import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.car.test.mocks.JavaMockitoHelper;
 import android.car.user.CarUserManager;
 import android.car.user.CarUserManager.UserLifecycleEvent;
 import android.car.user.CarUserManager.UserLifecycleListener;
@@ -64,13 +63,16 @@
 import org.mockito.Mock;
 
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
-public class CarUserNoticeServiceTest extends AbstractExtendedMockitoTestCase {
+public class CarUserNoticeServiceTest extends AbstractExtendedMockitoCarServiceTestCase {
+
+    private static final long TIMEOUT_MS = 10_000;
 
     @Mock
     private Context mMockContext;
     @Mock
+    private Context mOtherMockContext;
+    @Mock
     private Resources mMockedResources;
     @Mock
     private CarPowerManagementService mMockCarPowerManagementService;
@@ -110,22 +112,21 @@
     @Before
     public void setUpMocks() throws Exception {
         doReturn(mCarPowerManager).when(() -> CarLocalServices.createCarPowerManager(mMockContext));
-        doReturn(mMockCarPowerManagementService)
-                .when(() -> CarLocalServices.getService(CarPowerManagementService.class));
-        doReturn(mMockCarUserService)
-                .when(() -> CarLocalServices.getService(CarUserService.class));
+        mockGetCarLocalService(CarPowerManagementService.class, mMockCarPowerManagementService);
+        mockGetCarLocalService(CarUserService.class, mMockCarUserService);
 
         putSettingsInt(CarSettings.Secure.KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER, 1);
 
-        doReturn(mMockedResources).when(mMockContext).getResources();
-        doReturn(InstrumentationRegistry.getInstrumentation().getTargetContext()
-                .getContentResolver())
-                        .when(mMockContext).getContentResolver();
-        doReturn("com.foo/.Blah").when(mMockedResources).getString(anyInt());
-        doReturn(mMockPowerManager).when(mMockContext).getSystemService(PowerManager.class);
-        doReturn(mMockAppOpsManager).when(mMockContext).getSystemService(AppOpsManager.class);
-        doReturn(mMockPackageManager).when(mMockContext).getPackageManager();
-        doReturn(1).when(mMockPackageManager).getPackageUidAsUser(any(), anyInt());
+        when(mMockContext.getResources()).thenReturn(mMockedResources);
+        when(mMockContext.getContentResolver())
+                .thenReturn(InstrumentationRegistry.getInstrumentation().getTargetContext()
+                        .getContentResolver());
+        when(mMockedResources.getString(anyInt())).thenReturn("com.foo/.Blah");
+
+        mockContextGetService(mMockContext, PowerManager.class, mMockPowerManager);
+        mockContextGetService(mMockContext, AppOpsManager.class, mMockAppOpsManager);
+        mockContextGetService(mMockContext, PackageManager.class, mMockPackageManager);
+        when(mMockPackageManager.getPackageUidAsUser(any(), anyInt())).thenReturn(1);
         mCarUserNoticeService = new CarUserNoticeService(mMockContext, mHandler);
         mCarUserNoticeService.init();
         verify(mMockCarUserService).addUserLifecycleListener(
@@ -137,14 +138,14 @@
 
     @Test
     public void featureDisabledTest() {
-        Context mockContext = mock(Context.class);
-        // if feature is disabled, Resources.getString will return an
-        // empty string
-        doReturn("").when(mMockedResources).getString(R.string.config_userNoticeUiService);
-        doReturn(mMockedResources).when(mockContext).getResources();
-        CarUserNoticeService carUserNoticeService = new CarUserNoticeService(mockContext);
-        carUserNoticeService.init();
-        verify(mockContext, never()).registerReceiver(any(), any());
+        // Feature is disabled when the string is empty
+        when(mMockedResources.getString(R.string.config_userNoticeUiService)).thenReturn("");
+        when(mOtherMockContext.getResources()).thenReturn(mMockedResources);
+
+        CarUserNoticeService otherService = new CarUserNoticeService(mOtherMockContext);
+        otherService.init();
+
+        verify(mOtherMockContext, never()).registerReceiver(any(), any());
     }
 
     @Test
@@ -154,7 +155,7 @@
         setDisplayOff();
         CountDownLatch latch = mockUnbindService();
         sendBroadcast(Intent.ACTION_SCREEN_OFF);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
     }
 
     @Test
@@ -164,13 +165,13 @@
         setDisplayOff();
         CountDownLatch latch = mockUnbindService();
         sendBroadcast(Intent.ACTION_SCREEN_OFF);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
 
         // send screen on broadcast
         setDisplayOn();
         latch = mockBindService();
         sendBroadcast(Intent.ACTION_SCREEN_ON);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
     }
 
     @Test
@@ -180,7 +181,7 @@
         setDisplayOff();
         CountDownLatch latch = mockUnbindService();
         sendPowerStateChange(CarPowerManager.CarPowerStateListener.SHUTDOWN_PREPARE);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
     }
 
     @Test
@@ -190,13 +191,13 @@
         setDisplayOff();
         CountDownLatch latch = mockUnbindService();
         sendPowerStateChange(CarPowerManager.CarPowerStateListener.SHUTDOWN_PREPARE);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
 
         // send Power On
         setDisplayOn();
         latch = mockBindService();
         sendPowerStateChange(CarPowerManager.CarPowerStateListener.ON);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
     }
 
     @Test
@@ -206,20 +207,20 @@
         setDisplayOff();
         CountDownLatch latch = mockUnbindService();
         sendBroadcast(Intent.ACTION_SCREEN_OFF);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
 
         // UI not shown if key is disabled
         setDisplayOn();
         latch = mockKeySettings(
                 CarSettings.Secure.KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER, 0);
         sendBroadcast(Intent.ACTION_SCREEN_ON);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
         // invoked only once, when user switched
         verify(mMockContext, times(1)).bindServiceAsUser(any(), any(), anyInt(), any());
     }
 
-    private void assetLatchCalled(CountDownLatch latch) throws Exception {
-        assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
+    private static void assertLatchCalled(CountDownLatch latch) throws Exception {
+        JavaMockitoHelper.await(latch, TIMEOUT_MS);
     }
 
     private void switchUser(int userId) throws Exception {
@@ -260,11 +261,11 @@
     }
 
     private void setDisplayOn() {
-        doReturn(true).when(mMockPowerManager).isInteractive();
+        when(mMockPowerManager.isInteractive()).thenReturn(true);
     }
 
     private void setDisplayOff() {
-        doReturn(false).when(mMockPowerManager).isInteractive();
+        when(mMockPowerManager.isInteractive()).thenReturn(false);
     }
 
     private void sendBroadcast(String action) {
@@ -282,6 +283,6 @@
         setDisplayOn();
         CountDownLatch latch = mockBindService();
         switchUser(UserHandle.MIN_SECONDARY_USER_ID);
-        assetLatchCalled(latch);
+        assertLatchCalled(latch);
     }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
index f52e48d..87fdc18 100644
--- a/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.car.user;
 
+import static android.car.test.mocks.AndroidMockitoHelper.getResult;
+import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetSystemUser;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUserInfo;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUsers;
 import static android.car.test.util.UserTestingHelper.UserInfoBuilder;
@@ -54,6 +56,7 @@
 import android.car.CarOccupantZoneManager.OccupantZoneInfo;
 import android.car.settings.CarSettings;
 import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.car.test.mocks.AndroidMockitoHelper;
 import android.car.test.mocks.BlockingAnswer;
 import android.car.test.util.BlockingResultReceiver;
 import android.car.testapi.BlockingUserLifecycleListener;
@@ -61,12 +64,14 @@
 import android.car.user.CarUserManager.UserLifecycleEvent;
 import android.car.user.CarUserManager.UserLifecycleEventType;
 import android.car.user.CarUserManager.UserLifecycleListener;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
 import android.car.user.UserSwitchResult;
 import android.car.userlib.CarUserManagerHelper;
 import android.car.userlib.HalCallback;
 import android.car.userlib.UserHalHelper;
+import android.car.userlib.UserHelper;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
@@ -79,12 +84,15 @@
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociation;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationGetRequest;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationResponse;
+import android.hardware.automotive.vehicle.V2_0.UserIdentificationSetRequest;
 import android.hardware.automotive.vehicle.V2_0.UsersInfo;
 import android.location.LocationManager;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.sysprop.CarProperties;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -107,9 +115,8 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 
 /**
  * This class contains unit tests for the {@link CarUserService}.
@@ -141,9 +148,10 @@
     @Mock private Drawable mMockedDrawable;
     @Mock private UserMetrics mUserMetrics;
     @Mock IResultReceiver mSwitchUserUiReceiver;
+    @Mock PackageManager mPackageManager;
 
     private final BlockingUserLifecycleListener mUserLifecycleListener =
-            BlockingUserLifecycleListener.newDefaultListener();
+            BlockingUserLifecycleListener.forAnyEvent().build();
 
     @Captor private ArgumentCaptor<UsersInfo> mUsersInfoCaptor;
 
@@ -153,6 +161,8 @@
 
     private final int mGetUserInfoRequestType = InitialUserInfoRequestType.COLD_BOOT;
     private final AndroidFuture<UserSwitchResult> mUserSwitchFuture = new AndroidFuture<>();
+    private final AndroidFuture<UserIdentificationAssociationResponse> mUserAssociationRespFuture =
+            new AndroidFuture<>();
     private final int mAsyncCallTimeoutMs = 100;
     private final BlockingResultReceiver mReceiver =
             new BlockingResultReceiver(mAsyncCallTimeoutMs);
@@ -174,7 +184,12 @@
     @Override
     protected void onSessionBuilder(CustomMockitoSessionBuilder builder) {
         builder
-            .spyStatic(ActivityManager.class);
+            .spyStatic(ActivityManager.class)
+            // TODO(b/156299496): it cannot spy on UserManager, as it would slow down the tests
+            // considerably (more than 5 minutes total, instead of just a couple seconds). So, it's
+            // mocking UserHelper.isHeadlessSystemUser() (on mockIsHeadlessSystemUser()) instead...
+            .spyStatic(UserHelper.class)
+            .spyStatic(CarProperties.class);
     }
 
     /**
@@ -193,6 +208,7 @@
         doReturn(mMockedDrawable).when(mMockedDrawable).mutate();
         doReturn(1).when(mMockedDrawable).getIntrinsicWidth();
         doReturn(1).when(mMockedDrawable).getIntrinsicHeight();
+        doReturn(Optional.of(mAsyncCallTimeoutMs)).when(() -> CarProperties.user_hal_timeout());
         mCarUserService =
                 new CarUserService(
                         mMockContext,
@@ -218,7 +234,7 @@
     }
 
     @Test
-    public void testOnUserLifecycleEvent_nofityListener() throws Exception {
+    public void testOnUserLifecycleEvent_notifyListener() throws Exception {
         // Arrange
         mCarUserService.addUserLifecycleListener(mUserLifecycleListener);
         mockExistingUsers();
@@ -254,11 +270,19 @@
 
     private void verifyListenerOnEventInvoked(int expectedNewUserId, int expectedEventType)
             throws Exception {
-        UserLifecycleEvent actualEvent = mUserLifecycleListener.waitForEvent();
+        UserLifecycleEvent actualEvent = mUserLifecycleListener.waitForAnyEvent();
         assertThat(actualEvent.getEventType()).isEqualTo(expectedEventType);
         assertThat(actualEvent.getUserId()).isEqualTo(expectedNewUserId);
     }
 
+    private void verifyLastActiveUserSet(@UserIdInt int userId) {
+        verify(mMockedCarUserManagerHelper).setLastActiveUser(userId);
+    }
+
+    private void verifyLastActiveUserNotSet() {
+        verify(mMockedCarUserManagerHelper, never()).setLastActiveUser(anyInt());
+    }
+
     /**
      * Test that the {@link CarUserService} disables the location service for headless user 0 upon
      * first run.
@@ -271,13 +295,32 @@
     }
 
     /**
-     * Test that the {@link CarUserService} updates last active user on user switch.
+     * Test that the {@link CarUserService} updates last active user on user switch in non-headless
+     * system user mode.
      */
     @Test
-    public void testLastActiveUserUpdatedOnUserSwitch() throws Exception {
+    public void testLastActiveUserUpdatedOnUserSwitch_nonHeadlessSystemUser() throws Exception {
+        mockIsHeadlessSystemUser(mRegularUser.id, false);
         mockExistingUsers();
+
         sendUserSwitchingEvent(mAdminUser.id, mRegularUser.id);
-        verify(mMockedCarUserManagerHelper).setLastActiveUser(mRegularUser.id);
+
+        verifyLastActiveUserSet(mRegularUser.id);
+    }
+
+    /**
+     * Test that the {@link CarUserService} doesn't update last active user on user switch in
+     * headless system user mode.
+     */
+    @Test
+    public void testLastActiveUserUpdatedOnUserSwitch_headlessSystemUser() throws Exception {
+        mockIsHeadlessSystemUser(mRegularUser.id, true);
+        mockUmGetSystemUser(mMockedUserManager);
+        mockExistingUsers();
+
+        sendUserSwitchingEvent(mAdminUser.id, mRegularUser.id);
+
+        verifyLastActiveUserNotSet();
     }
 
     /**
@@ -489,34 +532,37 @@
     }
 
     @Test
-    public void testSwitchDriver() throws RemoteException {
-        int currentId = 11;
-        int targetId = 12;
-        mockGetCurrentUser(currentId);
-        doReturn(true).when(mMockedIActivityManager).switchUser(targetId);
-        doReturn(false).when(mMockedUserManager)
-                .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
-        assertTrue(mCarUserService.switchDriver(targetId));
+    public void testSwitchDriver() throws Exception {
+        mockExistingUsersAndCurrentUser(mAdminUser);
+        int requestId = 42;
+        mSwitchUserResponse.status = SwitchUserStatus.SUCCESS;
+        mSwitchUserResponse.requestId = requestId;
+        mockHalSwitch(mAdminUser.id, mRegularUser, mSwitchUserResponse);
+        mockAmSwitchUser(mRegularUser, true);
+        when(mMockedUserManager.hasUserRestriction(UserManager.DISALLOW_USER_SWITCH))
+                .thenReturn(false);
+        mCarUserService.switchDriver(mRegularUser.id, mUserSwitchFuture);
+        assertThat(getUserSwitchResult().getStatus())
+                .isEqualTo(UserSwitchResult.STATUS_SUCCESSFUL);
     }
 
     @Test
-    public void testSwitchDriver_IfUserSwitchIsNotAllowed() throws RemoteException {
-        int currentId = 11;
-        int targetId = 12;
-        mockGetCurrentUser(currentId);
-        doReturn(true).when(mMockedIActivityManager).switchUser(targetId);
-        doReturn(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED).when(mMockedUserManager)
-                .getUserSwitchability();
-        assertFalse(mCarUserService.switchDriver(targetId));
+    public void testSwitchDriver_IfUserSwitchIsNotAllowed() throws Exception {
+        when(mMockedUserManager.getUserSwitchability())
+                .thenReturn(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
+        mCarUserService.switchDriver(mRegularUser.id, mUserSwitchFuture);
+        assertThat(getUserSwitchResult().getStatus())
+                .isEqualTo(UserSwitchResult.STATUS_INVALID_REQUEST);
     }
 
     @Test
-    public void testSwitchDriver_IfSwitchedToCurrentUser() throws RemoteException {
-        int currentId = 11;
-        mockGetCurrentUser(currentId);
-        doReturn(false).when(mMockedUserManager)
-                .hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
-        assertTrue(mCarUserService.switchDriver(11));
+    public void testSwitchDriver_IfSwitchedToCurrentUser() throws Exception {
+        mockExistingUsersAndCurrentUser(mAdminUser);
+        when(mMockedUserManager.hasUserRestriction(UserManager.DISALLOW_USER_SWITCH))
+                .thenReturn(false);
+        mCarUserService.switchDriver(mAdminUser.id, mUserSwitchFuture);
+        assertThat(getUserSwitchResult().getStatus())
+                .isEqualTo(UserSwitchResult.STATUS_ALREADY_REQUESTED_USER);
     }
 
     @Test
@@ -571,7 +617,8 @@
                 new UserInfo(15, "test15", UserInfo.FLAG_EPHEMERAL),
                 new UserInfo(16, "test16", UserInfo.FLAG_DISABLED),
                 new UserInfo(17, "test17", UserInfo.FLAG_MANAGED_PROFILE),
-                new UserInfo(18, "test18", UserInfo.FLAG_MANAGED_PROFILE)
+                new UserInfo(18, "test18", UserInfo.FLAG_MANAGED_PROFILE),
+                new UserInfo(19, "test19", NO_USER_INFO_FLAGS)
         ));
         // Parent: test10, child: test12
         associateParentChild(users.get(0), users.get(2));
@@ -585,12 +632,13 @@
     @Test
     public void testGetAllPossibleDrivers() {
         Set<Integer> expected = new HashSet<Integer>(Arrays.asList(10, 11, 13, 14));
-        doReturn(prepareUserList()).when(mMockedUserManager).getUsers(true);
+        when(mMockedUserManager.getUsers(true)).thenReturn(prepareUserList());
+        mockIsHeadlessSystemUser(19, true);
         for (UserInfo user : mCarUserService.getAllDrivers()) {
-            assertTrue(expected.contains(user.id));
+            assertThat(expected).contains(user.id);
             expected.remove(user.id);
         }
-        assertEquals(0, expected.size());
+        assertThat(expected).isEmpty();
     }
 
     @Test
@@ -600,18 +648,19 @@
                 put(0, new HashSet<Integer>());
                 put(10, new HashSet<Integer>(Arrays.asList(12)));
                 put(11, new HashSet<Integer>());
-                put(13, new HashSet<Integer>(Arrays.asList(17, 18)));
+                put(13, new HashSet<Integer>(Arrays.asList(17)));
             }
         };
+        mockIsHeadlessSystemUser(18, true);
         for (int i = 0; i < testCases.size(); i++) {
-            doReturn(prepareUserList()).when(mMockedUserManager).getUsers(true);
+            when(mMockedUserManager.getUsers(true)).thenReturn(prepareUserList());
             List<UserInfo> passengers = mCarUserService.getPassengers(testCases.keyAt(i));
             HashSet<Integer> expected = testCases.valueAt(i);
             for (UserInfo user : passengers) {
-                assertTrue(expected.contains(user.id));
+                assertThat(expected).contains(user.id);
                 expected.remove(user.id);
             }
-            assertEquals(0, expected.size());
+            assertThat(expected).isEmpty();
         }
     }
 
@@ -939,6 +988,8 @@
     @Test
     public void testSetSwitchUserUI_receiverSetAndCalled() throws Exception {
         mockExistingUsersAndCurrentUser(mAdminUser);
+        int callerId = Binder.getCallingUid();
+        mockCallerUid(callerId, true);
         int requestId = 42;
         mSwitchUserResponse.status = SwitchUserStatus.SUCCESS;
         mSwitchUserResponse.requestId = requestId;
@@ -953,6 +1004,39 @@
     }
 
     @Test
+    public void testSetSwitchUserUI_nonCarSysUiCaller() throws Exception {
+        int callerId = Binder.getCallingUid();
+        mockCallerUid(callerId, false);
+
+        assertThrows(SecurityException.class,
+                () -> mCarUserService.setUserSwitchUiCallback(mSwitchUserUiReceiver));
+    }
+
+    @Test
+    public void testSwitchUser_OEMRequest_success() throws Exception {
+        mockExistingUsersAndCurrentUser(mAdminUser);
+        mockAmSwitchUser(mRegularUser, true);
+        int requestId = -1;
+
+        mCarUserService.switchAndroidUserFromHal(requestId, mRegularUser.id);
+        mockCurrentUser(mRegularUser);
+        sendUserUnlockedEvent(mRegularUser.id);
+
+        assertPostSwitch(requestId, mRegularUser.id, mRegularUser.id);
+    }
+
+    @Test
+    public void testSwitchUser_OEMRequest_failure() throws Exception {
+        mockExistingUsersAndCurrentUser(mAdminUser);
+        mockAmSwitchUser(mRegularUser, false);
+        int requestId = -1;
+
+        mCarUserService.switchAndroidUserFromHal(requestId, mRegularUser.id);
+
+        assertPostSwitch(requestId, mAdminUser.id, mRegularUser.id);
+    }
+
+    @Test
     public void testGetUserInfo_nullReceiver() throws Exception {
         assertThrows(NullPointerException.class, () -> mCarUserService
                 .getInitialUserInfo(mGetUserInfoRequestType, mAsyncCallTimeoutMs, null));
@@ -978,6 +1062,24 @@
         Bundle resultData = mReceiver.getResultData();
         assertThat(resultData).isNotNull();
         assertInitialInfoAction(resultData, mGetUserInfoResponse.action);
+        assertInitialInfoUserLocales(resultData, null);
+    }
+
+    @Test
+    public void testGetUserInfo_defaultResponse_withLocale() throws Exception {
+        mockExistingUsersAndCurrentUser(mAdminUser);
+
+        mGetUserInfoResponse.action = InitialUserInfoResponseAction.DEFAULT;
+        mGetUserInfoResponse.userLocales = "LOL";
+        mockGetInitialInfo(mAdminUser.id, mGetUserInfoResponse);
+
+        mCarUserService.getInitialUserInfo(mGetUserInfoRequestType, mAsyncCallTimeoutMs, mReceiver);
+
+        assertThat(mReceiver.getResultCode()).isEqualTo(HalCallback.STATUS_OK);
+        Bundle resultData = mReceiver.getResultData();
+        assertThat(resultData).isNotNull();
+        assertInitialInfoAction(resultData, mGetUserInfoResponse.action);
+        assertInitialInfoUserLocales(resultData, "LOL");
     }
 
     @Test
@@ -1099,39 +1201,133 @@
 
     @Test
     public void testGetUserIdentificationAssociation_service_returnNull() throws Exception {
-        // Must use the real user id - and not mock it - as the service will infer the id from
-        // the Binder call - it's not worth the effort of mocking that.
-        int currentUserId = ActivityManager.getCurrentUser();
-        Log.d(TAG, "testGetUserIdentificationAssociation_ok(): current user is " + currentUserId);
-        UserInfo currentUser = mockUmGetUserInfo(mMockedUserManager, currentUserId,
-                UserInfo.FLAG_ADMIN);
+        mockCurrentUserForBinderCalls();
 
         // Not mocking service call, so it will return null
 
-        GetUserIdentificationAssociationResponse response = mCarUserService
+        UserIdentificationAssociationResponse response = mCarUserService
                 .getUserIdentificationAssociation(new int[] { 108 });
 
-        assertThat(response).isNull();
+        assertThat(response.isSuccess()).isFalse();
+        assertThat(response.getValues()).isNull();
+        assertThat(response.getErrorMessage()).isNull();
     }
 
     @Test
     public void testGetUserIdentificationAssociation_ok() throws Exception {
-        // Must use the real user id - and not mock it - as the service will infer the id from
-        // the Binder call - it's not worth the effort of mocking that.
-        int currentUserId = ActivityManager.getCurrentUser();
-        Log.d(TAG, "testGetUserIdentificationAssociation_ok(): current user is " + currentUserId);
-        UserInfo currentUser = mockUmGetUserInfo(mMockedUserManager, currentUserId,
-                UserInfo.FLAG_ADMIN);
+        UserInfo currentUser = mockCurrentUserForBinderCalls();
 
         int[] types = new int[] { 1, 2, 3 };
-        mockHalGetUserIdentificationAssociation(currentUser, types, new int[] { 10, 20, 30 },
-                "D'OH!");
+        int[] values = new int[] { 10, 20, 30 };
+        mockHalGetUserIdentificationAssociation(currentUser, types, values, "D'OH!");
 
-        GetUserIdentificationAssociationResponse response = mCarUserService
+        UserIdentificationAssociationResponse response = mCarUserService
                 .getUserIdentificationAssociation(types);
 
-        assertThat(response.getValues()).asList().containsExactly(10, 20, 30)
-                .inOrder();
+        assertThat(response.isSuccess()).isTrue();
+        assertThat(response.getValues()).asList().containsAllOf(10, 20, 30).inOrder();
+        assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_nullTypes() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mCarUserService
+                .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+                        null, new int[] {42}, mUserAssociationRespFuture));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_emptyTypes() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mCarUserService
+                .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+                        new int[0], new int[] {42}, mUserAssociationRespFuture));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_nullValues() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mCarUserService
+                .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+                        new int[] {42}, null, mUserAssociationRespFuture));
+    }
+    @Test
+    public void testSetUserIdentificationAssociation_sizeMismatch() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mCarUserService
+                .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+                        new int[] {1}, new int[] {2, 2}, mUserAssociationRespFuture));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_nullFuture() throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> mCarUserService
+                .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+                        new int[] {42}, new int[] {42}, null));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_noPermission() throws Exception {
+        mockManageUsersPermission(android.Manifest.permission.MANAGE_USERS, false);
+        assertThrows(SecurityException.class, () -> mCarUserService
+                .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+                        new int[] {42}, new int[] {42}, mUserAssociationRespFuture));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_noCurrentUser() throws Exception {
+        // Should fail because we're not mocking UserManager.getUserInfo() to set the flag
+        assertThrows(IllegalArgumentException.class, () -> mCarUserService
+                .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+                        new int[] {42}, new int[] {42}, mUserAssociationRespFuture));
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_halFailedWithErrorMessage() throws Exception {
+        mockCurrentUserForBinderCalls();
+        mockHalSetUserIdentificationAssociationFailure("D'OH!");
+        int[] types = new int[] { 1, 2, 3 };
+        int[] values = new int[] { 10, 20, 30 };
+        mCarUserService.setUserIdentificationAssociation(mAsyncCallTimeoutMs, types, values,
+                mUserAssociationRespFuture);
+
+        UserIdentificationAssociationResponse response = getUserAssociationRespResult();
+
+        assertThat(response.isSuccess()).isFalse();
+        assertThat(response.getValues()).isNull();
+        assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
+
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_halFailedWithoutErrorMessage()
+            throws Exception {
+        mockCurrentUserForBinderCalls();
+        mockHalSetUserIdentificationAssociationFailure(/* errorMessage= */ null);
+        int[] types = new int[] { 1, 2, 3 };
+        int[] values = new int[] { 10, 20, 30 };
+        mCarUserService.setUserIdentificationAssociation(mAsyncCallTimeoutMs, types, values,
+                mUserAssociationRespFuture);
+
+        UserIdentificationAssociationResponse response = getUserAssociationRespResult();
+
+        assertThat(response.isSuccess()).isFalse();
+        assertThat(response.getValues()).isNull();
+        assertThat(response.getErrorMessage()).isNull();
+    }
+
+    @Test
+    public void testSetUserIdentificationAssociation_ok() throws Exception {
+        UserInfo currentUser = mockCurrentUserForBinderCalls();
+
+        int[] types = new int[] { 1, 2, 3 };
+        int[] values = new int[] { 10, 20, 30 };
+        mockHalSetUserIdentificationAssociationSuccess(currentUser, types, values, "D'OH!");
+
+        mCarUserService.setUserIdentificationAssociation(mAsyncCallTimeoutMs, types, values,
+                mUserAssociationRespFuture);
+
+        UserIdentificationAssociationResponse response = getUserAssociationRespResult();
+
+        assertThat(response.isSuccess()).isTrue();
+        assertThat(response.getValues()).asList().containsAllOf(10, 20, 30).inOrder();
         assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
     }
 
@@ -1161,12 +1357,22 @@
     }
 
     @NonNull
-    private <T> T getResult(@NonNull AndroidFuture<T> future) throws Exception {
-        try {
-            return future.get(mAsyncCallTimeoutMs, TimeUnit.MILLISECONDS);
-        } catch (TimeoutException e) {
-            throw new IllegalStateException("not called in " + mAsyncCallTimeoutMs + "ms", e);
-        }
+    private UserIdentificationAssociationResponse getUserAssociationRespResult()
+            throws Exception {
+        return getResult(mUserAssociationRespFuture);
+    }
+
+    /**
+     * This method must be called for cases where the service infers the user id of the caller
+     * using Binder - it's not worth the effort of mocking such (native) calls.
+     */
+    @NonNull
+    private UserInfo mockCurrentUserForBinderCalls() {
+        int currentUserId = ActivityManager.getCurrentUser();
+        Log.d(TAG, "testetUserIdentificationAssociation_ok(): current user is " + currentUserId);
+        UserInfo currentUser = mockUmGetUserInfo(mMockedUserManager, currentUserId,
+                UserInfo.FLAG_ADMIN);
+        return currentUser;
     }
 
     /**
@@ -1181,7 +1387,7 @@
     private void mockExistingUsers() {
         mockUmGetUsers(mMockedUserManager, mExistingUsers);
         for (UserInfo user : mExistingUsers) {
-            when(mMockedUserManager.getUserInfo(user.id)).thenReturn(user);
+            AndroidMockitoHelper.mockUmGetUserInfo(mMockedUserManager, user);
         }
     }
 
@@ -1208,11 +1414,29 @@
                 eq(usersInfo), notNull());
     }
 
+    private void mockIsHeadlessSystemUser(@UserIdInt int userId, boolean mode) {
+        doReturn(mode).when(() -> UserHelper.isHeadlessSystemUser(userId));
+    }
+
     private void mockHalSwitch(@UserIdInt int currentUserId, @NonNull UserInfo androidTargetUser,
             @Nullable SwitchUserResponse response) {
         mockHalSwitch(currentUserId, HalCallback.STATUS_OK, response, androidTargetUser);
     }
 
+    private void mockCallerUid(int uid, boolean returnCorrectUid) throws Exception {
+        String packageName = "packageName";
+        String className = "className";
+        when(mMockedResources.getString(anyInt())).thenReturn(packageName + "/" + className);
+        when(mMockContext.createContextAsUser(any(), anyInt())).thenReturn(mMockContext);
+        when(mMockContext.getPackageManager()).thenReturn(mPackageManager);
+
+        if (returnCorrectUid) {
+            when(mPackageManager.getPackageUid(any(), anyInt())).thenReturn(uid);
+        } else {
+            when(mPackageManager.getPackageUid(any(), anyInt())).thenReturn(uid + 1);
+        }
+    }
+
     private BlockingAnswer<Void> mockHalSwitchLateResponse(@UserIdInt int currentUserId,
             @NonNull UserInfo androidTargetUser, @Nullable SwitchUserResponse response) {
         android.hardware.automotive.vehicle.V2_0.UserInfo halTargetUser =
@@ -1272,6 +1496,53 @@
                 .thenReturn(response);
     }
 
+    private void mockHalSetUserIdentificationAssociationSuccess(@NonNull UserInfo user,
+            @NonNull int[] types, @NonNull int[] values,  @Nullable String errorMessage) {
+        assertWithMessage("mismatch on number of types and values").that(types.length)
+                .isEqualTo(values.length);
+
+        UserIdentificationResponse response = new UserIdentificationResponse();
+        response.numberAssociation = types.length;
+        response.errorMessage = errorMessage;
+        for (int i = 0; i < types.length; i++) {
+            UserIdentificationAssociation association = new UserIdentificationAssociation();
+            association.type = types[i];
+            association.value = values[i];
+            response.associations.add(association);
+        }
+
+        doAnswer((invocation) -> {
+            Log.d(TAG, "Answering " + invocation + " with " + response);
+            @SuppressWarnings("unchecked")
+            UserIdentificationSetRequest request =
+                    (UserIdentificationSetRequest) invocation.getArguments()[1];
+            assertWithMessage("Wrong user on %s", request)
+                    .that(request.userInfo.userId)
+                    .isEqualTo(user.id);
+            assertWithMessage("Wrong flags on %s", request)
+                    .that(UserHalHelper.toUserInfoFlags(request.userInfo.flags))
+                    .isEqualTo(user.flags);
+            @SuppressWarnings("unchecked")
+            HalCallback<UserIdentificationResponse> callback =
+                    (HalCallback<UserIdentificationResponse>) invocation.getArguments()[2];
+            callback.onResponse(HalCallback.STATUS_OK, response);
+            return null;
+        }).when(mUserHal).setUserAssociation(eq(mAsyncCallTimeoutMs), notNull(), notNull());
+    }
+
+    private void mockHalSetUserIdentificationAssociationFailure(@NonNull String errorMessage) {
+        UserIdentificationResponse response = new UserIdentificationResponse();
+        response.errorMessage = errorMessage;
+        doAnswer((invocation) -> {
+            Log.d(TAG, "Answering " + invocation + " with " + response);
+            @SuppressWarnings("unchecked")
+            HalCallback<UserIdentificationResponse> callback =
+                    (HalCallback<UserIdentificationResponse>) invocation.getArguments()[2];
+            callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, response);
+            return null;
+        }).when(mUserHal).setUserAssociation(eq(mAsyncCallTimeoutMs), notNull(), notNull());
+    }
+
     private void mockManageUsersPermission(String permission, boolean granted) {
         int result;
         if (granted) {
@@ -1383,6 +1654,13 @@
                 .isEqualTo(expectedAction);
     }
 
+    private void assertInitialInfoUserLocales(Bundle resultData, String expectedLocales) {
+        String actualLocales = resultData.getString(CarUserService.BUNDLE_USER_LOCALES);
+        assertWithMessage("wrong locales on bundle extra %s",
+                CarUserService.BUNDLE_USER_LOCALES).that(actualLocales)
+                .isEqualTo(expectedLocales);
+    }
+
     private void assertNoPostSwitch() {
         verify(mUserHal, never()).postSwitchResponse(anyInt(), any(), any());
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/user/ExperimentalCarUserManagerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/user/ExperimentalCarUserManagerUnitTest.java
index 026754c..8b4dc55 100644
--- a/tests/carservice_unit_test/src/com/android/car/user/ExperimentalCarUserManagerUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/user/ExperimentalCarUserManagerUnitTest.java
@@ -15,10 +15,15 @@
  */
 package com.android.car.user;
 
+import static android.car.test.mocks.AndroidMockitoHelper.getResult;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.when;
 
 import android.annotation.UserIdInt;
@@ -28,10 +33,14 @@
 import android.car.test.util.UserTestingHelper;
 import android.car.user.CarUserManager;
 import android.car.user.ExperimentalCarUserManager;
+import android.car.user.UserSwitchResult;
 import android.content.pm.UserInfo;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 
+import com.android.internal.infra.AndroidFuture;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -92,16 +101,19 @@
 
     @Test
     public void testSwitchDriver_Success() throws Exception {
-        expectSwitchDriverSucceed();
-        boolean success = mManager.switchDriver(10);
-        assertThat(success).isTrue();
+        expectSwitchDriverSucceed(10);
+        AndroidFuture<UserSwitchResult> future = mManager.switchDriver(10);
+        UserSwitchResult result = getResult(future);
+        assertThat(result.getStatus()).isEqualTo(UserSwitchResult.STATUS_SUCCESSFUL);
     }
 
     @Test
     public void testSwitchDriver_Error() throws Exception {
-        expectSwitchDriverFail();
-        boolean success = mManager.switchDriver(20);
-        assertThat(success).isFalse();
+        expectSwitchDriverFail(20);
+        AndroidFuture<UserSwitchResult> future = mManager.switchDriver(20);
+        assertThat(future).isNotNull();
+        UserSwitchResult result = getResult(future);
+        assertThat(result.getStatus()).isEqualTo(UserSwitchResult.STATUS_HAL_INTERNAL_FAILURE);
     }
 
     @Test
@@ -171,12 +183,19 @@
         when(mService.createPassenger("test passenger", /* driverId = */ 10)).thenReturn(null);
     }
 
-    private void expectSwitchDriverSucceed() throws Exception {
-        when(mService.switchDriver(10)).thenReturn(true);
+    private void expectSwitchDriverSucceed(@UserIdInt int userId) throws Exception {
+        doAnswer((invocation) -> {
+            @SuppressWarnings("unchecked")
+            AndroidFuture<UserSwitchResult> future = (AndroidFuture<UserSwitchResult>) invocation
+                    .getArguments()[1];
+            future.complete(new UserSwitchResult(UserSwitchResult.STATUS_SUCCESSFUL, null));
+            return null;
+        }).when(mService).switchDriver(eq(userId), notNull());
     }
 
-    private void expectSwitchDriverFail() throws Exception {
-        when(mService.switchDriver(20)).thenReturn(false);
+    private void expectSwitchDriverFail(@UserIdInt int userId) throws Exception {
+        doThrow(new RemoteException("D'OH!")).when(mService)
+            .switchDriver(eq(userId), notNull());
     }
 
     private void expectStartPassengerSucceed() throws Exception {
diff --git a/tests/carservice_unit_test/src/com/android/car/user/UserIdentificationAssociationResponseTest.java b/tests/carservice_unit_test/src/com/android/car/user/UserIdentificationAssociationResponseTest.java
new file mode 100644
index 0000000..f9ca16d
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/user/UserIdentificationAssociationResponseTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+package com.android.car.user;
+
+import static android.car.user.UserIdentificationAssociationResponse.forFailure;
+import static android.car.user.UserIdentificationAssociationResponse.forSuccess;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.car.user.UserIdentificationAssociationResponse;
+
+import org.junit.Test;
+
+public final class UserIdentificationAssociationResponseTest {
+
+    @Test
+    public void testFailure_noMessage() {
+        UserIdentificationAssociationResponse response = forFailure();
+
+        assertThat(response).isNotNull();
+        assertThat(response.isSuccess()).isFalse();
+        assertThat(response.getErrorMessage()).isNull();
+        assertThat(response.getValues()).isNull();
+    }
+
+    @Test
+    public void testFailure_withMessage() {
+        UserIdentificationAssociationResponse response = forFailure("D'OH!");
+
+        assertThat(response).isNotNull();
+        assertThat(response.isSuccess()).isFalse();
+        assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
+        assertThat(response.getValues()).isNull();
+    }
+
+    @Test
+    public void testSuccess_nullValues() {
+        assertThrows(IllegalArgumentException.class, () -> forSuccess(null));
+    }
+
+    @Test
+    public void testSuccess_nullValuesWithMessage() {
+        assertThrows(IllegalArgumentException.class, () -> forSuccess(null, "D'OH!"));
+    }
+
+    @Test
+    public void testSuccess_emptyValues() {
+        assertThrows(IllegalArgumentException.class, () -> forSuccess(new int[0]));
+    }
+
+    @Test
+    public void testSuccess_emptyValuesWithMessage() {
+        assertThrows(IllegalArgumentException.class, () -> forSuccess(new int[0], "D'OH!"));
+    }
+
+    @Test
+    public void testSuccess_noMessage() {
+        UserIdentificationAssociationResponse response = forSuccess(new int[] {1, 2, 3});
+
+        assertThat(response).isNotNull();
+        assertThat(response.isSuccess()).isTrue();
+        assertThat(response.getErrorMessage()).isNull();
+        assertThat(response.getValues()).asList().containsAllOf(1, 2, 3).inOrder();
+    }
+
+    @Test
+    public void testSuccess_withMessage() {
+        UserIdentificationAssociationResponse response = forSuccess(new int[] {1, 2, 3}, "D'OH!");
+
+        assertThat(response).isNotNull();
+        assertThat(response.isSuccess()).isTrue();
+        assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
+        assertThat(response.getValues()).asList().containsAllOf(1, 2, 3).inOrder();
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/vms/VmsBrokerServiceTest.java b/tests/carservice_unit_test/src/com/android/car/vms/VmsBrokerServiceTest.java
index dd1c02f..e85dece 100644
--- a/tests/carservice_unit_test/src/com/android/car/vms/VmsBrokerServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsBrokerServiceTest.java
@@ -19,8 +19,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -46,6 +49,8 @@
 import android.os.RemoteException;
 import android.os.SharedMemory;
 import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 
@@ -63,6 +68,7 @@
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -106,14 +112,14 @@
     @Mock
     private IVmsClientCallback mClientCallback1;
     @Mock
-    private Binder mClientBinder1;
+    private IBinder mClientBinder1;
     @Mock
     private VmsClientLogger mClientLog1;
 
     @Mock
     private IVmsClientCallback mClientCallback2;
     @Mock
-    private Binder mClientBinder2;
+    private IBinder mClientBinder2;
     @Mock
     private VmsClientLogger mClientLog2;
 
@@ -126,6 +132,8 @@
 
     private VmsBrokerService mBrokerService;
     private int mCallingAppUid;
+    private Map<IBinder /* token */, Pair<IBinder /* callback */, IBinder.DeathRecipient>>
+            mDeathRecipients = new ArrayMap<>();
 
     @Before
     public void setUp() throws Exception {
@@ -160,7 +168,7 @@
     @Test
     public void testRegister() {
         VmsRegistrationInfo registrationInfo =
-                mBrokerService.registerClient(mClientToken1, mClientCallback1, false);
+                registerClient(mClientToken1, mClientCallback1, false);
 
         verify(mClientLog1).logConnectionState(VmsClientLogger.ConnectionState.CONNECTED);
         assertThat(registrationInfo.getAvailableLayers()).isEqualTo(DEFAULT_AVAILABLE_LAYERS);
@@ -168,9 +176,20 @@
     }
 
     @Test
+    public void testRegister_DeadCallback() throws Exception {
+        doThrow(RemoteException.class).when(mClientBinder1).linkToDeath(any(), anyInt());
+        assertThrows(
+                IllegalStateException.class,
+                () -> registerClient(mClientToken1, mClientCallback1));
+
+        verify(mClientLog1).logConnectionState(VmsClientLogger.ConnectionState.CONNECTED);
+        verify(mClientLog1).logConnectionState(VmsClientLogger.ConnectionState.DISCONNECTED);
+    }
+
+    @Test
     public void testRegister_LegacyClient() {
         VmsRegistrationInfo registrationInfo =
-                mBrokerService.registerClient(mClientToken1, mClientCallback1, true);
+                registerClient(mClientToken1, mClientCallback1, true);
 
         verify(mClientLog1).logConnectionState(VmsClientLogger.ConnectionState.CONNECTED);
         assertThat(registrationInfo.getAvailableLayers()).isEqualTo(DEFAULT_AVAILABLE_LAYERS);
@@ -178,11 +197,22 @@
     }
 
     @Test
+    public void testRegister_LegacyClient_DeadCallback() throws Exception {
+        doThrow(RemoteException.class).when(mClientBinder1).linkToDeath(any(), anyInt());
+        assertThrows(
+                IllegalStateException.class,
+                () -> registerClient(mClientToken1, mClientCallback1, true));
+
+        verify(mClientLog1).logConnectionState(VmsClientLogger.ConnectionState.CONNECTED);
+        verify(mClientLog1).logConnectionState(VmsClientLogger.ConnectionState.DISCONNECTED);
+    }
+
+    @Test
     public void testRegister_TwoClients_OneProcess() {
         VmsRegistrationInfo registrationInfo =
-                mBrokerService.registerClient(mClientToken1, mClientCallback1, false);
+                registerClient(mClientToken1, mClientCallback1, false);
         VmsRegistrationInfo registrationInfo2 =
-                mBrokerService.registerClient(mClientToken2, mClientCallback2, false);
+                registerClient(mClientToken2, mClientCallback2, false);
 
         verify(mClientLog1, times(2))
                 .logConnectionState(VmsClientLogger.ConnectionState.CONNECTED);
@@ -192,10 +222,10 @@
     @Test
     public void testRegister_TwoClients_TwoProcesses() {
         VmsRegistrationInfo registrationInfo =
-                mBrokerService.registerClient(mClientToken1, mClientCallback1, false);
+                registerClient(mClientToken1, mClientCallback1, false);
         mCallingAppUid = TEST_APP_UID2;
         VmsRegistrationInfo registrationInfo2 =
-                mBrokerService.registerClient(mClientToken2, mClientCallback2, false);
+                registerClient(mClientToken2, mClientCallback2, false);
 
         verify(mClientLog1).logConnectionState(VmsClientLogger.ConnectionState.CONNECTED);
         verify(mClientLog2).logConnectionState(VmsClientLogger.ConnectionState.CONNECTED);
@@ -214,7 +244,7 @@
         ));
 
         VmsRegistrationInfo registrationInfo =
-                mBrokerService.registerClient(mClientToken2, mClientCallback2, false);
+                registerClient(mClientToken2, mClientCallback2, false);
 
         VmsAvailableLayers expectedLayers = new VmsAvailableLayers(1, asSet(
                 new VmsAssociatedLayer(LAYER1,
@@ -230,6 +260,15 @@
     }
 
     @Test
+    public void testUnregisterClient_UnknownClient() throws Exception {
+        registerClient(mClientToken1, mClientCallback1);
+        mBrokerService.unregisterClient(mClientToken2);
+
+        verify(mClientCallback1, never()).onLayerAvailabilityChanged(any());
+        verify(mClientCallback1, never()).onSubscriptionStateChanged(any());
+    }
+
+    @Test
     public void testRegisterProvider_UnknownClient() {
         assertThrows(
                 IllegalStateException.class,
@@ -441,7 +480,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER2, emptySet())
         ));
-        mBrokerService.unregisterClient(mClientToken2);
+        unregisterClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(3,
                 asSet(LAYER1),
@@ -461,7 +500,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER1, emptySet())
         ));
-        mBrokerService.unregisterClient(mClientToken2);
+        unregisterClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(1,
                 asSet(LAYER1),
@@ -481,7 +520,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER2, emptySet())
         ));
-        disconnectClient(mClientCallback2);
+        disconnectClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(3,
                 asSet(LAYER1),
@@ -501,7 +540,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER1, emptySet())
         ));
-        disconnectClient(mClientCallback2);
+        disconnectClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(1,
                 asSet(LAYER1),
@@ -756,7 +795,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER2, asSet(54321))
         ));
-        mBrokerService.unregisterClient(mClientToken2);
+        unregisterClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(3,
                 emptySet(),
@@ -776,7 +815,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER1, asSet(54321))
         ));
-        mBrokerService.unregisterClient(mClientToken2);
+        unregisterClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(3,
                 emptySet(),
@@ -796,7 +835,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER1, asSet(12345))
         ));
-        mBrokerService.unregisterClient(mClientToken2);
+        unregisterClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(1,
                 emptySet(),
@@ -816,7 +855,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER2, asSet(54321))
         ));
-        disconnectClient(mClientCallback2);
+        disconnectClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(3,
                 emptySet(),
@@ -836,7 +875,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER1, asSet(54321))
         ));
-        disconnectClient(mClientCallback2);
+        disconnectClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(3,
                 emptySet(),
@@ -856,7 +895,7 @@
         mBrokerService.setSubscriptions(mClientToken2, asList(
                 new VmsAssociatedLayer(LAYER1, asSet(12345))
         ));
-        disconnectClient(mClientCallback2);
+        disconnectClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(1,
                 emptySet(),
@@ -1033,7 +1072,7 @@
                 new VmsAssociatedLayer(LAYER2, emptySet()),
                 new VmsAssociatedLayer(LAYER1, asSet(54321))
         ));
-        mBrokerService.unregisterClient(mClientToken2);
+        unregisterClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(3,
                 asSet(LAYER1),
@@ -1055,7 +1094,7 @@
                 new VmsAssociatedLayer(LAYER2, emptySet()),
                 new VmsAssociatedLayer(LAYER1, asSet(54321))
         ));
-        disconnectClient(mClientCallback2);
+        disconnectClient(mClientToken2);
 
         VmsSubscriptionState expectedSubscriptions = new VmsSubscriptionState(3,
                 asSet(LAYER1),
@@ -1222,7 +1261,7 @@
 
     @Test
     public void testSetProviderOfferings_UnknownProviderId_LegacyClient() throws Exception {
-        mBrokerService.registerClient(mClientToken1, mClientCallback1, true);
+        registerClient(mClientToken1, mClientCallback1, true);
 
         mBrokerService.setProviderOfferings(mClientToken1, 12345, asList(
                 new VmsLayerDependency(LAYER1)
@@ -1249,7 +1288,7 @@
     public void testSetProviderOfferings_OtherClientsProviderId_LegacyClient() throws Exception {
         registerClient(mClientToken1, mClientCallback1);
         int providerId = mBrokerService.registerProvider(mClientToken1, PROVIDER_INFO1);
-        mBrokerService.registerClient(mClientToken2, mClientCallback2, true);
+        registerClient(mClientToken2, mClientCallback2, true);
 
         mBrokerService.setProviderOfferings(mClientToken2, providerId, asList(
                 new VmsLayerDependency(LAYER1)
@@ -1610,7 +1649,7 @@
         mBrokerService.setProviderOfferings(mClientToken1, providerId, asList(
                 new VmsLayerDependency(LAYER1)
         ));
-        mBrokerService.unregisterClient(mClientToken1);
+        unregisterClient(mClientToken1);
 
         VmsAvailableLayers expectedLayers1 = new VmsAvailableLayers(1, asSet(
                 new VmsAssociatedLayer(LAYER1, asSet(providerId)))
@@ -1636,7 +1675,7 @@
         mBrokerService.setProviderOfferings(mClientToken1, providerId2, asList(
                 new VmsLayerDependency(LAYER1)
         ));
-        mBrokerService.unregisterClient(mClientToken1);
+        unregisterClient(mClientToken1);
 
         VmsAvailableLayers expectedLayers1 = new VmsAvailableLayers(2, asSet(
                 new VmsAssociatedLayer(LAYER1, asSet(providerId, providerId2)))
@@ -1661,7 +1700,7 @@
         mBrokerService.setProviderOfferings(mClientToken2, providerId2, asList(
                 new VmsLayerDependency(LAYER1)
         ));
-        mBrokerService.unregisterClient(mClientToken1);
+        unregisterClient(mClientToken1);
 
         VmsAvailableLayers expectedLayers1 = new VmsAvailableLayers(2, asSet(
                 new VmsAssociatedLayer(LAYER1, asSet(providerId, providerId2)))
@@ -1688,7 +1727,7 @@
         mBrokerService.setProviderOfferings(mClientToken2, providerId, asList(
                 new VmsLayerDependency(LAYER1)
         ));
-        disconnectClient(mClientCallback1);
+        disconnectClient(mClientToken1);
 
         VmsAvailableLayers expectedLayers1 = new VmsAvailableLayers(1, asSet(
                 new VmsAssociatedLayer(LAYER1, asSet(providerId)))
@@ -1712,7 +1751,7 @@
         mBrokerService.setProviderOfferings(mClientToken1, providerId, asList(
                 new VmsLayerDependency(LAYER1)
         ));
-        disconnectClient(mClientCallback1);
+        disconnectClient(mClientToken1);
 
         VmsAvailableLayers expectedLayers1 = new VmsAvailableLayers(1, asSet(
                 new VmsAssociatedLayer(LAYER1, asSet(providerId)))
@@ -1738,7 +1777,7 @@
         mBrokerService.setProviderOfferings(mClientToken1, providerId2, asList(
                 new VmsLayerDependency(LAYER1)
         ));
-        disconnectClient(mClientCallback1);
+        disconnectClient(mClientToken1);
 
         VmsAvailableLayers expectedLayers1 = new VmsAvailableLayers(2, asSet(
                 new VmsAssociatedLayer(LAYER1, asSet(providerId, providerId2)))
@@ -1763,7 +1802,7 @@
         mBrokerService.setProviderOfferings(mClientToken2, providerId2, asList(
                 new VmsLayerDependency(LAYER1)
         ));
-        disconnectClient(mClientCallback1);
+        disconnectClient(mClientToken1);
 
         VmsAvailableLayers expectedLayers1 = new VmsAvailableLayers(2, asSet(
                 new VmsAssociatedLayer(LAYER1, asSet(providerId, providerId2)))
@@ -1790,7 +1829,7 @@
         mBrokerService.setProviderOfferings(mClientToken2, providerId, asList(
                 new VmsLayerDependency(LAYER1)
         ));
-        disconnectClient(mClientCallback1);
+        disconnectClient(mClientToken1);
 
         VmsAvailableLayers expectedLayers1 = new VmsAvailableLayers(1, asSet(
                 new VmsAssociatedLayer(LAYER1, asSet(providerId)))
@@ -2506,7 +2545,7 @@
 
     @Test
     public void testPublishPacket_UnknownOffering_LegacyClient() throws Exception {
-        mBrokerService.registerClient(mClientToken1, mClientCallback1, true);
+        registerClient(mClientToken1, mClientCallback1, true);
 
         mBrokerService.setSubscriptions(mClientToken1, asList(
                 new VmsAssociatedLayer(LAYER1, emptySet())
@@ -2840,7 +2879,7 @@
     @Test
     public void testPublishLargePacket_UnknownOffering_LegacyClient() throws Exception {
         setupLargePacket();
-        mBrokerService.registerClient(mClientToken1, mClientCallback1, true);
+        registerClient(mClientToken1, mClientCallback1, true);
 
         mBrokerService.setSubscriptions(mClientToken1, asList(
                 new VmsAssociatedLayer(LAYER1, emptySet())
@@ -3210,14 +3249,46 @@
     }
 
     private void registerClient(IBinder token, IVmsClientCallback callback) {
-        mBrokerService.registerClient(token, callback, false);
+        registerClient(token, callback, false);
     }
 
-    private static void disconnectClient(IVmsClientCallback callback) throws Exception {
-        ArgumentCaptor<IBinder.DeathRecipient> deathRecipient =
-                ArgumentCaptor.forClass(IBinder.DeathRecipient.class);
-        verify(callback.asBinder()).linkToDeath(deathRecipient.capture(), eq(0));
-        deathRecipient.getValue().binderDied();
+    private VmsRegistrationInfo registerClient(IBinder token, IVmsClientCallback callback,
+            boolean legacyClient) {
+        VmsRegistrationInfo registrationInfo =
+                mBrokerService.registerClient(token, callback, legacyClient);
+
+        IBinder callbackBinder = callback.asBinder();
+        try {
+            if (!mDeathRecipients.containsKey(token)) {
+                ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor =
+                        ArgumentCaptor.forClass(IBinder.DeathRecipient.class);
+                verify(callbackBinder).linkToDeath(deathRecipientCaptor.capture(), eq(0));
+                mDeathRecipients.put(token,
+                        Pair.create(callbackBinder, deathRecipientCaptor.getValue()));
+            } else {
+                verify(callbackBinder, never()).linkToDeath(any(), anyInt());
+            }
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+
+        return registrationInfo;
+    }
+
+    private void unregisterClient(IBinder token) {
+        mBrokerService.unregisterClient(token);
+
+        Pair<IBinder, IBinder.DeathRecipient> deathRecipientPair = mDeathRecipients.get(token);
+        assertThat(deathRecipientPair).isNotNull();
+        verify(deathRecipientPair.first).unlinkToDeath(same(deathRecipientPair.second), eq(0));
+    }
+
+    private void disconnectClient(IBinder token) {
+        Pair<IBinder, IBinder.DeathRecipient> deathRecipientPair = mDeathRecipients.get(token);
+        assertThat(deathRecipientPair).isNotNull();
+
+        deathRecipientPair.second.binderDied();
+        verify(deathRecipientPair.first).unlinkToDeath(same(deathRecipientPair.second), eq(0));
     }
 
     private static void verifyLayerAvailability(
@@ -3252,6 +3323,7 @@
             int providerId, VmsLayer layer, byte[] payload) throws RemoteException {
         verify(callback).onPacketReceived(providerId, layer, payload);
     }
+
     private static void verifyLargePacketReceived(
             IVmsClientCallback callback,
             int providerId, VmsLayer layer, SharedMemory packet) throws RemoteException {
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsLayersAvailabilityTest.java b/tests/carservice_unit_test/src/com/android/car/vms/VmsLayerAvailabilityTest.java
similarity index 78%
rename from tests/carservice_unit_test/src/com/android/car/VmsLayersAvailabilityTest.java
rename to tests/carservice_unit_test/src/com/android/car/vms/VmsLayerAvailabilityTest.java
index ebf50d8..9010802 100644
--- a/tests/carservice_unit_test/src/com/android/car/VmsLayersAvailabilityTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsLayerAvailabilityTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.car;
+package com.android.car.vms;
 
 import android.car.vms.VmsAssociatedLayer;
 import android.car.vms.VmsLayer;
@@ -29,7 +29,7 @@
 import java.util.Set;
 
 @SmallTest
-public class VmsLayersAvailabilityTest extends AndroidTestCase {
+public class VmsLayerAvailabilityTest extends AndroidTestCase {
 
     private static final VmsLayer LAYER_X = new VmsLayer(1, 1, 2);
     private static final VmsLayer LAYER_Y = new VmsLayer(3, 2, 4);
@@ -38,38 +38,38 @@
     private static final int PUBLISHER_ID_1 = 19;
     private static final int PUBLISHER_ID_2 = 28;
 
-    private static final Set<Integer> PUBLISHERS_1 = new HashSet<>(Arrays.asList(PUBLISHER_ID_1));
-    private static final Set<Integer> PUBLISHERS_2 = new HashSet<>(Arrays.asList(PUBLISHER_ID_2));
+    private static final Set<Integer> PUBLISHERS_1 = Collections.singleton(PUBLISHER_ID_1);
+    private static final Set<Integer> PUBLISHERS_2 = Collections.singleton(PUBLISHER_ID_2);
     private static final Set<Integer> PUBLISHERS_1_AND_2 =
             new HashSet<>(Arrays.asList(PUBLISHER_ID_1, PUBLISHER_ID_2));
 
     private static final VmsLayerDependency X_DEPENDS_ON_Y =
-            new VmsLayerDependency(LAYER_X, new HashSet<VmsLayer>(Arrays.asList(LAYER_Y)));
+            new VmsLayerDependency(LAYER_X, Collections.singleton(LAYER_Y));
 
     private static final VmsLayerDependency X_DEPENDS_ON_Z =
-            new VmsLayerDependency(LAYER_X, new HashSet<VmsLayer>(Arrays.asList(LAYER_Z)));
+            new VmsLayerDependency(LAYER_X, Collections.singleton(LAYER_Z));
 
     private static final VmsLayerDependency Y_DEPENDS_ON_Z =
-            new VmsLayerDependency(LAYER_Y, new HashSet<VmsLayer>(Arrays.asList(LAYER_Z)));
+            new VmsLayerDependency(LAYER_Y, Collections.singleton(LAYER_Z));
 
     private static final VmsLayerDependency Y_DEPENDS_ON_X =
-            new VmsLayerDependency(LAYER_Y, new HashSet<VmsLayer>(Arrays.asList(LAYER_X)));
+            new VmsLayerDependency(LAYER_Y, Collections.singleton(LAYER_X));
 
     private static final VmsLayerDependency Z_DEPENDS_ON_X =
-            new VmsLayerDependency(LAYER_Z, new HashSet<VmsLayer>(Arrays.asList(LAYER_X)));
+            new VmsLayerDependency(LAYER_Z, Collections.singleton(LAYER_X));
 
     private static final VmsLayerDependency Z_DEPENDS_ON_NOTHING =
             new VmsLayerDependency(LAYER_Z);
 
     private static final VmsLayerDependency X_DEPENDS_ON_SELF =
-            new VmsLayerDependency(LAYER_X, new HashSet<VmsLayer>(Arrays.asList(LAYER_X)));
+            new VmsLayerDependency(LAYER_X, Collections.singleton(LAYER_X));
 
     private Set<VmsLayersOffering> mOfferings;
-    private VmsLayersAvailability mLayersAvailability;
+    private VmsLayerAvailability mLayersAvailability;
 
     @Override
     protected void setUp() throws Exception {
-        mLayersAvailability = new VmsLayersAvailability();
+        mLayersAvailability = new VmsLayerAvailability();
         mOfferings = new HashSet<>();
         super.setUp();
     }
@@ -79,16 +79,16 @@
     }
 
     public void testEmptyOffering() {
-        mLayersAvailability.setPublishersOffering(Collections.EMPTY_LIST);
+        mLayersAvailability.setPublishersOffering(Collections.emptyList());
         assertTrue(mLayersAvailability.getAvailableLayers().getAssociatedLayers().isEmpty());
     }
 
-    public void testSingleLayerNoDeps() throws Exception {
+    public void testSingleLayerNoDeps() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_X, PUBLISHERS_2));
 
         VmsLayersOffering offering =
-                new VmsLayersOffering(new HashSet<>(Arrays.asList(new VmsLayerDependency(LAYER_X))),
+                new VmsLayersOffering(Collections.singleton(new VmsLayerDependency(LAYER_X)),
                         PUBLISHER_ID_2);
 
         mOfferings.add(offering);
@@ -98,7 +98,7 @@
                 mLayersAvailability.getAvailableLayers().getAssociatedLayers());
     }
 
-    public void testChainOfDependenciesSatisfied() throws Exception {
+    public void testChainOfDependenciesSatisfied() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_X, PUBLISHERS_1));
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_Y, PUBLISHERS_1));
@@ -114,11 +114,11 @@
         mLayersAvailability.setPublishersOffering(mOfferings);
 
         assertEquals(expectedAvailableAssociatedLayers,
-                new HashSet<VmsAssociatedLayer>(
+                new HashSet<>(
                         mLayersAvailability.getAvailableLayers().getAssociatedLayers()));
     }
 
-    public void testChainOfDependenciesSatisfiedTwoOfferings() throws Exception {
+    public void testChainOfDependenciesSatisfiedTwoOfferings() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_X, PUBLISHERS_1));
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_Y, PUBLISHERS_1));
@@ -130,7 +130,7 @@
                         PUBLISHER_ID_1);
 
         VmsLayersOffering offering2 =
-                new VmsLayersOffering(new HashSet<>(Arrays.asList(Z_DEPENDS_ON_NOTHING)),
+                new VmsLayersOffering(Collections.singleton(Z_DEPENDS_ON_NOTHING),
                         PUBLISHER_ID_1);
 
         mOfferings.add(offering1);
@@ -138,11 +138,11 @@
         mLayersAvailability.setPublishersOffering(mOfferings);
 
         assertEquals(expectedAvailableAssociatedLayers,
-                new HashSet<VmsAssociatedLayer>(
+                new HashSet<>(
                         mLayersAvailability.getAvailableLayers().getAssociatedLayers()));
     }
 
-    public void testChainOfDependencieNotSatisfied() throws Exception {
+    public void testChainOfDependenciesNotSatisfied() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
         VmsLayersOffering offering =
                 new VmsLayersOffering(new HashSet<>(Arrays.asList(X_DEPENDS_ON_Y, Y_DEPENDS_ON_Z)),
@@ -152,11 +152,11 @@
         mLayersAvailability.setPublishersOffering(mOfferings);
 
         assertEquals(expectedAvailableAssociatedLayers,
-                new HashSet<VmsAssociatedLayer>(
+                new HashSet<>(
                         mLayersAvailability.getAvailableLayers().getAssociatedLayers()));
     }
 
-    public void testOneOfMultipleDependencySatisfied() throws Exception {
+    public void testOneOfMultipleDependencySatisfied() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_X, PUBLISHERS_1));
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_Z, PUBLISHERS_1));
@@ -172,11 +172,11 @@
         mLayersAvailability.setPublishersOffering(mOfferings);
 
         assertEquals(expectedAvailableAssociatedLayers,
-                new HashSet<VmsAssociatedLayer>(
+                new HashSet<>(
                         mLayersAvailability.getAvailableLayers().getAssociatedLayers()));
     }
 
-    public void testCyclicDependency() throws Exception {
+    public void testCyclicDependency() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
 
         VmsLayersOffering offering =
@@ -189,11 +189,11 @@
         mLayersAvailability.setPublishersOffering(mOfferings);
 
         assertEquals(expectedAvailableAssociatedLayers,
-                new HashSet<VmsAssociatedLayer>(
+                new HashSet<>(
                         mLayersAvailability.getAvailableLayers().getAssociatedLayers()));
     }
 
-    public void testAlmostCyclicDependency() throws Exception {
+    public void testAlmostCyclicDependency() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_Z, PUBLISHERS_1_AND_2));
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_X, PUBLISHERS_1));
@@ -216,7 +216,7 @@
                 mLayersAvailability.getAvailableLayers().getAssociatedLayers());
     }
 
-    public void testCyclicDependencyAndLayerWithoutDependency() throws Exception {
+    public void testCyclicDependencyAndLayerWithoutDependency() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
         expectedAvailableAssociatedLayers.add(new VmsAssociatedLayer(LAYER_Z, PUBLISHERS_1));
 
@@ -227,29 +227,29 @@
                         PUBLISHER_ID_1);
 
         VmsLayersOffering offering2 =
-                new VmsLayersOffering(new HashSet<>(Arrays.asList(Y_DEPENDS_ON_X)), PUBLISHER_ID_2);
+                new VmsLayersOffering(Collections.singleton(Y_DEPENDS_ON_X), PUBLISHER_ID_2);
 
         mOfferings.add(offering1);
         mOfferings.add(offering2);
         mLayersAvailability.setPublishersOffering(mOfferings);
 
         assertEquals(expectedAvailableAssociatedLayers,
-                new HashSet<VmsAssociatedLayer>(
+                new HashSet<>(
                         mLayersAvailability.getAvailableLayers().getAssociatedLayers()));
     }
 
-    public void testSelfDependency() throws Exception {
+    public void testSelfDependency() {
         Set<VmsAssociatedLayer> expectedAvailableAssociatedLayers = new HashSet<>();
 
         VmsLayersOffering offering =
-                new VmsLayersOffering(new HashSet<>(Arrays.asList(X_DEPENDS_ON_SELF)),
+                new VmsLayersOffering(Collections.singleton(X_DEPENDS_ON_SELF),
                         PUBLISHER_ID_1);
 
         mOfferings.add(offering);
         mLayersAvailability.setPublishersOffering(mOfferings);
 
         assertEquals(expectedAvailableAssociatedLayers,
-                new HashSet<VmsAssociatedLayer>(
+                new HashSet<>(
                         mLayersAvailability.getAvailableLayers().getAssociatedLayers()));
     }
-}
\ No newline at end of file
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/vms/VmsProviderInfoStoreTest.java b/tests/carservice_unit_test/src/com/android/car/vms/VmsProviderInfoStoreTest.java
new file mode 100644
index 0000000..ae28fd1
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsProviderInfoStoreTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+package com.android.car.vms;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class VmsProviderInfoStoreTest {
+    private static final byte[] MOCK_INFO_1 = new byte[]{2, 3, 5, 7, 11, 13, 17};
+    private static final byte[] SAME_MOCK_INFO_1 = new byte[]{2, 3, 5, 7, 11, 13, 17};
+    private static final byte[] MOCK_INFO_2 = new byte[]{2, 3, 5, 7, 11, 13, 17, 19};
+
+    private VmsProviderInfoStore mProviderInfoStore;
+
+    @Before
+    public void setUp() throws Exception {
+        mProviderInfoStore = new VmsProviderInfoStore();
+    }
+
+    @Test
+    public void testSingleInfo() {
+        int id = mProviderInfoStore.getProviderId(MOCK_INFO_1);
+        assertEquals(1, id);
+        assertArrayEquals(MOCK_INFO_1, mProviderInfoStore.getProviderInfo(id));
+    }
+
+    @Test
+    public void testSingleInfo_NoSuchId() {
+        assertNull(mProviderInfoStore.getProviderInfo(12345));
+    }
+
+    @Test
+    public void testTwoInfos() {
+        int id1 = mProviderInfoStore.getProviderId(MOCK_INFO_1);
+        int id2 = mProviderInfoStore.getProviderId(MOCK_INFO_2);
+        assertEquals(1, id1);
+        assertEquals(2, id2);
+        assertArrayEquals(MOCK_INFO_1, mProviderInfoStore.getProviderInfo(id1));
+        assertArrayEquals(MOCK_INFO_2, mProviderInfoStore.getProviderInfo(id2));
+    }
+
+    @Test
+    public void testSingleInfoInsertedTwice() {
+        int id = mProviderInfoStore.getProviderId(MOCK_INFO_1);
+        assertEquals(1, id);
+
+        int sameId = mProviderInfoStore.getProviderId(SAME_MOCK_INFO_1);
+        assertEquals(sameId, id);
+    }
+}
diff --git a/user/car-user-lib/src/android/car/userlib/CommonConstants.java b/user/car-user-lib/src/android/car/userlib/CommonConstants.java
index ef9a354..89c09c5 100644
--- a/user/car-user-lib/src/android/car/userlib/CommonConstants.java
+++ b/user/car-user-lib/src/android/car/userlib/CommonConstants.java
@@ -33,6 +33,7 @@
         public static final String BUNDLE_USER_ID = "user.id";
         public static final String BUNDLE_USER_FLAGS = "user.flags";
         public static final String BUNDLE_USER_NAME = "user.name";
+        public static final String BUNDLE_USER_LOCALES = "user.locales";
         public static final String BUNDLE_INITIAL_INFO_ACTION = "initial_info.action";
 
         private CarUserServiceConstants() {
diff --git a/user/car-user-lib/src/android/car/userlib/InitialUserSetter.java b/user/car-user-lib/src/android/car/userlib/InitialUserSetter.java
index d3e5d52..10b642b 100644
--- a/user/car-user-lib/src/android/car/userlib/InitialUserSetter.java
+++ b/user/car-user-lib/src/android/car/userlib/InitialUserSetter.java
@@ -18,6 +18,7 @@
 import static android.car.userlib.UserHalHelper.userFlagsToString;
 import static android.car.userlib.UserHelper.safeName;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -30,6 +31,8 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
+import android.sysprop.CarProperties;
 import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
@@ -40,6 +43,8 @@
 import com.android.internal.widget.LockPatternUtils;
 
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.function.Consumer;
 
 /**
@@ -49,8 +54,50 @@
 
     private static final String TAG = InitialUserSetter.class.getSimpleName();
 
-    // TODO(b/151758646): STOPSHIP if not false
-    private static final boolean DBG = true;
+    private static final boolean DBG = false;
+
+    /**
+     * Sets the initial user using the default behavior.
+     *
+     * <p>The default behavior is:
+     *
+     * <ol>
+     *  <li>On first boot, it creates and switches to a new user.
+     *  <li>Otherwise, it will switch to either:
+     *  <ol>
+     *   <li>User defined by {@code android.car.systemuser.bootuseroverrideid} (when it was
+     * constructed with such option enabled).
+     *   <li>Last active user (as defined by
+     * {@link android.provider.Settings.Global.LAST_ACTIVE_USER_ID}.
+     *  </ol>
+     * </ol>
+     */
+    public static final int TYPE_DEFAULT_BEHAVIOR = 0;
+
+    /**
+     * Switches to the given user, falling back to {@link #fallbackDefaultBehavior(String)} if it
+     * fails.
+     */
+    public static final int TYPE_SWITCH = 1;
+
+    /**
+     * Creates a new user and switches to it, falling back to
+     * {@link #fallbackDefaultBehavior(String) if any of these steps fails.
+     *
+     * @param name (optional) name of the new user
+     * @param halFlags user flags as defined by Vehicle HAL ({@code UserFlags} enum).
+     */
+    public static final int TYPE_CREATE = 2;
+
+    @IntDef(prefix = { "TYPE_" }, value = {
+            TYPE_DEFAULT_BEHAVIOR,
+            TYPE_SWITCH,
+            TYPE_CREATE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface InitialUserInfoType { }
+
+    private final Context mContext;
 
     // TODO(b/150413304): abstract AM / UM into interfaces, then provide local and remote
     // implementation (where local is implemented by ActivityManagerInternal / UserManagerInternal)
@@ -58,74 +105,218 @@
     private final UserManager mUm;
     private final LockPatternUtils mLockPatternUtils;
 
-    // TODO(b/151758646): make sure it's unit tested
-    private final boolean mSupportsOverrideUserIdProperty;
-
     private final String mNewUserName;
     private final String mNewGuestName;
 
     private final Consumer<UserInfo> mListener;
 
-    public InitialUserSetter(@NonNull Context context, @NonNull Consumer<UserInfo> listener,
-            boolean supportsOverrideUserIdProperty) {
-        this(context, listener, /* newGuestName= */ null, supportsOverrideUserIdProperty);
+    public InitialUserSetter(@NonNull Context context, @NonNull Consumer<UserInfo> listener) {
+        this(context, listener, /* newGuestName= */ null);
     }
 
     public InitialUserSetter(@NonNull Context context, @NonNull Consumer<UserInfo> listener,
-            @Nullable String newGuestName, boolean supportsOverrideUserIdProperty) {
-        this(new CarUserManagerHelper(context), UserManager.get(context), listener,
+            @Nullable String newGuestName) {
+        this(context, new CarUserManagerHelper(context), UserManager.get(context), listener,
                 new LockPatternUtils(context),
-                context.getString(com.android.internal.R.string.owner_name), newGuestName,
-                supportsOverrideUserIdProperty);
+                context.getString(com.android.internal.R.string.owner_name), newGuestName);
     }
 
     @VisibleForTesting
-    public InitialUserSetter(@NonNull CarUserManagerHelper helper, @NonNull UserManager um,
-            @NonNull Consumer<UserInfo> listener, @NonNull LockPatternUtils lockPatternUtils,
-            @Nullable String newUserName, @Nullable String newGuestName,
-            boolean supportsOverrideUserIdProperty) {
+    public InitialUserSetter(@NonNull Context context, @NonNull CarUserManagerHelper helper,
+            @NonNull UserManager um, @NonNull Consumer<UserInfo> listener,
+            @NonNull LockPatternUtils lockPatternUtils,
+            @Nullable String newUserName, @Nullable String newGuestName) {
+        mContext = context;
         mHelper = helper;
         mUm = um;
         mListener = listener;
         mLockPatternUtils = lockPatternUtils;
         mNewUserName = newUserName;
         mNewGuestName = newGuestName;
-        mSupportsOverrideUserIdProperty = supportsOverrideUserIdProperty;
     }
 
     /**
-     * Sets the initial user using the default behavior.
+     * Builder for {@link InitialUserInfo} objects.
      *
-     * <p>The default behavior is:
-     * <ol>
-     *   <li>On first boot, it creates and switches to a new user.
-     *   <li>Otherwise, it will switch to either:
-     *   <ol>
-     *     <li>User defined by {@code android.car.systemuser.bootuseroverrideid} (when it was
-     *       constructed with such option enabled).
-     *     <li>Last active user (as defined by
-     *       {@link android.provider.Settings..Global.LAST_ACTIVE_USER_ID}.
-     *   </ol>
-     * </ol>
      */
-    public void executeDefaultBehavior(boolean replaceGuest) {
-        executeDefaultBehavior(replaceGuest, /* fallback= */ false);
-    }
+    public static final class Builder {
 
-    private void executeDefaultBehavior(boolean replaceGuest, boolean fallback) {
-        if (!mHelper.hasInitialUser()) {
-            if (DBG) Log.d(TAG, "executeDefaultBehavior(): no initial user, creating it");
-            createAndSwitchUser(mNewUserName, UserFlags.ADMIN, fallback);
-        } else {
-            if (DBG) Log.d(TAG, "executeDefaultBehavior(): switching to initial user");
-            int userId = mHelper.getInitialUser(mSupportsOverrideUserIdProperty);
-            switchUser(userId, replaceGuest, fallback);
+        private final @InitialUserInfoType int mType;
+        private boolean mReplaceGuest;
+        private @UserIdInt int mSwitchUserId;
+        private @Nullable String mNewUserName;
+        private int mNewUserFlags;
+        private boolean mSupportsOverrideUserIdProperty;
+        private @Nullable String mUserLocales;
+
+        /**
+         * Constructor for the given type.
+         *
+         * @param type {@link #TYPE_DEFAULT_BEHAVIOR}, {@link #TYPE_SWITCH},
+         * or {@link #TYPE_CREATE}.
+         */
+        public Builder(@InitialUserInfoType int type) {
+            Preconditions.checkArgument(
+                    type == TYPE_DEFAULT_BEHAVIOR || type == TYPE_SWITCH || type == TYPE_CREATE,
+                    "invalid builder type");
+            mType = type;
+        }
+
+        /**
+         * Sets the id of the user to be switched to.
+         *
+         * @throws IllegalArgumentException if builder is not for {@link #TYPE_SWITCH}.
+         */
+        @NonNull
+        public Builder setSwitchUserId(@UserIdInt int userId) {
+            Preconditions.checkArgument(mType == TYPE_SWITCH, "invalid builder type: " + mType);
+            mSwitchUserId = userId;
+            return this;
+        }
+
+        /**
+         * Sets whether the current user should be replaced when it's a guest.
+         */
+        @NonNull
+        public Builder setReplaceGuest(boolean value) {
+            mReplaceGuest = value;
+            return this;
+        }
+
+        /**
+         * Sets the name of the new user being created.
+         *
+         * @throws IllegalArgumentException if builder is not for {@link #TYPE_CREATE}.
+         */
+        @NonNull
+        public Builder setNewUserName(@Nullable String name) {
+            Preconditions.checkArgument(mType == TYPE_CREATE, "invalid builder type: " + mType);
+            mNewUserName = name;
+            return this;
+        }
+
+        /**
+         * Sets the flags (as defined by {@link android.hardware.automotive.vehicle.V2_0.UserFlags})
+         * of the new user being created.
+         *
+         * @throws IllegalArgumentException if builder is not for {@link #TYPE_CREATE}.
+         */
+        @NonNull
+        public Builder setNewUserFlags(int flags) {
+            Preconditions.checkArgument(mType == TYPE_CREATE, "invalid builder type: " + mType);
+            mNewUserFlags = flags;
+            return this;
+        }
+
+        /**
+         * Sets whether the {@link CarProperties#boot_user_override_id()} should be taking in
+         * account when using the default behavior.
+         */
+        @NonNull
+        public Builder setSupportsOverrideUserIdProperty(boolean value) {
+            mSupportsOverrideUserIdProperty = value;
+            return this;
+        }
+
+        /**
+         * Sets the system locales for the initial user (when it's created).
+         */
+        @NonNull
+        public Builder setUserLocales(@Nullable String userLocales) {
+            mUserLocales = userLocales;
+            return this;
+        }
+
+        /**
+         * Builds the object.
+         */
+        @NonNull
+        public InitialUserInfo build() {
+            return new InitialUserInfo(this);
         }
     }
 
+    /**
+     * Object used to define the properties of the initial user (which can then be set by
+     * {@link InitialUserSetter#set(InitialUserInfo)});
+     */
+    public static final class InitialUserInfo {
+        public final @InitialUserInfoType int type;
+        public final boolean replaceGuest;
+        public final @UserIdInt int switchUserId;
+        public final @Nullable String newUserName;
+        public final int newUserFlags;
+        public final boolean supportsOverrideUserIdProperty;
+        public @Nullable String userLocales;
+
+        private InitialUserInfo(@NonNull Builder builder) {
+            type = builder.mType;
+            switchUserId = builder.mSwitchUserId;
+            replaceGuest = builder.mReplaceGuest;
+            newUserName = builder.mNewUserName;
+            newUserFlags = builder.mNewUserFlags;
+            supportsOverrideUserIdProperty = builder.mSupportsOverrideUserIdProperty;
+            userLocales = builder.mUserLocales;
+        }
+    }
+
+    /**
+     * Sets the initial user.
+     */
+    public void set(@NonNull InitialUserInfo info) {
+        Preconditions.checkArgument(info != null, "info cannot be null");
+
+        switch (info.type) {
+            case TYPE_DEFAULT_BEHAVIOR:
+                executeDefaultBehavior(info, /* fallback= */ false);
+                break;
+            case TYPE_SWITCH:
+                try {
+                    switchUser(info, /* fallback= */ true);
+                } catch (Exception e) {
+                    fallbackDefaultBehavior(info, /* fallback= */ true,
+                            "Exception switching user: " + e);
+                }
+                break;
+            case TYPE_CREATE:
+                try {
+                    createAndSwitchUser(info, /* fallback= */ true);
+                } catch (Exception e) {
+                    fallbackDefaultBehavior(info, /* fallback= */ true,
+                            "Exception createUser user with name "
+                                    + UserHelper.safeName(info.newUserName) + " and flags "
+                                    + UserHalHelper.userFlagsToString(info.newUserFlags) + ": "
+                                    + e);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("invalid InitialUserInfo type: " + info.type);
+        }
+    }
+
+    private void executeDefaultBehavior(@NonNull InitialUserInfo info, boolean fallback) {
+        if (!mHelper.hasInitialUser()) {
+            if (DBG) Log.d(TAG, "executeDefaultBehavior(): no initial user, creating it");
+            createAndSwitchUser(new Builder(TYPE_CREATE)
+                    .setNewUserName(mNewUserName)
+                    .setNewUserFlags(UserFlags.ADMIN)
+                    .setSupportsOverrideUserIdProperty(info.supportsOverrideUserIdProperty)
+                    .setUserLocales(info.userLocales)
+                    .build(), fallback);
+        } else {
+            if (DBG) Log.d(TAG, "executeDefaultBehavior(): switching to initial user");
+            int userId = mHelper.getInitialUser(info.supportsOverrideUserIdProperty);
+            switchUser(new Builder(TYPE_SWITCH)
+                    .setSwitchUserId(userId)
+                    .setSupportsOverrideUserIdProperty(info.supportsOverrideUserIdProperty)
+                    .setReplaceGuest(info.replaceGuest)
+                    .build(), fallback);
+        }
+    }
 
     @VisibleForTesting
-    void fallbackDefaultBehavior(boolean fallback, @NonNull String reason) {
+    void fallbackDefaultBehavior(@NonNull InitialUserInfo info, boolean fallback,
+            @NonNull String reason) {
         if (!fallback) {
             // Only log the error
             Log.w(TAG, reason);
@@ -134,22 +325,13 @@
             return;
         }
         Log.w(TAG, "Falling back to default behavior. Reason: " + reason);
-        executeDefaultBehavior(/* replaceGuest= */ true, /* fallback= */ false);
+        executeDefaultBehavior(info, /* fallback= */ false);
     }
 
-    /**
-     * Switches to the given user, falling back to {@link #fallbackDefaultBehavior(String)} if it
-     * fails.
-     */
-    public void switchUser(@UserIdInt int userId, boolean replaceGuest) {
-        try {
-            switchUser(userId, replaceGuest, /* fallback= */ true);
-        } catch (Exception e) {
-            fallbackDefaultBehavior(/* fallback= */ true, "Exception switching user: " + e);
-        }
-    }
+    private void switchUser(@NonNull InitialUserInfo info, boolean fallback) {
+        int userId = info.switchUserId;
+        boolean replaceGuest = info.replaceGuest;
 
-    private void switchUser(@UserIdInt int userId, boolean replaceGuest, boolean fallback) {
         if (DBG) {
             Log.d(TAG, "switchUser(): userId=" + userId + ", replaceGuest=" + replaceGuest
                     + ", fallback=" + fallback);
@@ -157,7 +339,7 @@
 
         UserInfo user = mUm.getUserInfo(userId);
         if (user == null) {
-            fallbackDefaultBehavior(fallback, "user with id " + userId + " doesn't exist");
+            fallbackDefaultBehavior(info, fallback, "user with id " + userId + " doesn't exist");
             return;
         }
 
@@ -167,7 +349,8 @@
             actualUser = replaceGuestIfNeeded(user);
 
             if (actualUser == null) {
-                fallbackDefaultBehavior(fallback, "could not replace guest " + user.toFullString());
+                fallbackDefaultBehavior(info, fallback, "could not replace guest "
+                        + user.toFullString());
                 return;
             }
         }
@@ -179,7 +362,8 @@
         int currentUserId = ActivityManager.getCurrentUser();
         if (actualUserId != currentUserId) {
             if (!startForegroundUser(actualUserId)) {
-                fallbackDefaultBehavior(fallback, "am.switchUser(" + actualUserId + ") failed");
+                fallbackDefaultBehavior(info, fallback,
+                        "am.switchUser(" + actualUserId + ") failed");
                 return;
             }
             mHelper.setLastActiveUser(actualUserId);
@@ -242,7 +426,10 @@
             Log.w(TAG, "failed to mark guest " + user.id + " for deletion");
         }
 
-        Pair<UserInfo, String> result = createNewUser(mNewGuestName, halFlags);
+        Pair<UserInfo, String> result = createNewUser(new Builder(TYPE_CREATE)
+                .setNewUserName(mNewGuestName)
+                .setNewUserFlags(halFlags)
+                .build());
 
         String errorMessage = result.second;
         if (errorMessage != null) {
@@ -253,31 +440,18 @@
         return result.first;
     }
 
-    /**
-     * Creates a new user and switches to it, falling back to
-     * {@link #fallbackDefaultBehavior(String) if any of these steps fails.
-     *
-     * @param name (optional) name of the new user
-     * @param halFlags user flags as defined by Vehicle HAL ({@code UserFlags} enum).
-     */
-    public void createUser(@Nullable String name, int halFlags) {
-        try {
-            createAndSwitchUser(name, halFlags, /* fallback= */ true);
-        } catch (Exception e) {
-            fallbackDefaultBehavior(/* fallback= */ true, "Exception createUser user with flags "
-                    + UserHalHelper.userFlagsToString(halFlags) + ": " + e);
-        }
-    }
-
-    private void createAndSwitchUser(@Nullable String name, int halFlags, boolean fallback) {
-        Pair<UserInfo, String> result = createNewUser(name, halFlags);
+    private void createAndSwitchUser(@NonNull InitialUserInfo info, boolean fallback) {
+        Pair<UserInfo, String> result = createNewUser(info);
         String reason = result.second;
         if (reason != null) {
-            fallbackDefaultBehavior(fallback, reason);
+            fallbackDefaultBehavior(info, fallback, reason);
             return;
         }
 
-        switchUser(result.first.id, /* replaceGuest= */ false, fallback);
+        switchUser(new Builder(TYPE_SWITCH)
+                .setSwitchUserId(result.first.id)
+                .setSupportsOverrideUserIdProperty(info.supportsOverrideUserIdProperty)
+                .build(), fallback);
     }
 
     /**
@@ -287,7 +461,10 @@
      * error message.
      */
     @NonNull
-    private Pair<UserInfo, String> createNewUser(@Nullable String name, int halFlags) {
+    private Pair<UserInfo, String> createNewUser(@NonNull InitialUserInfo info) {
+        String name = info.newUserName;
+        int halFlags = info.newUserFlags;
+
         if (DBG) {
             Log.d(TAG, "createUser(name=" + safeName(name) + ", flags="
                     + userFlagsToString(halFlags) + ")");
@@ -330,6 +507,15 @@
         }
 
         if (DBG) Log.d(TAG, "user created: " + userInfo.id);
+
+        if (info.userLocales != null) {
+            if (DBG) {
+                Log.d(TAG, "setting locale for user " + userInfo.id + " to " + info.userLocales);
+            }
+            Settings.System.putStringForUser(mContext.getContentResolver(),
+                    Settings.System.SYSTEM_LOCALES, info.userLocales, userInfo.id);
+        }
+
         return new Pair<>(userInfo, null);
     }
 
@@ -390,8 +576,6 @@
     public void dump(@NonNull PrintWriter writer) {
         writer.println("InitialUserSetter");
         String indent = "  ";
-        writer.printf("%smSupportsOverrideUserIdProperty: %s\n", indent,
-                mSupportsOverrideUserIdProperty);
         writer.printf("%smNewUserName: %s\n", indent, mNewUserName);
         writer.printf("%smNewGuestName: %s\n", indent, mNewGuestName);
     }
diff --git a/user/car-user-lib/src/android/car/userlib/UserHalHelper.java b/user/car-user-lib/src/android/car/userlib/UserHalHelper.java
index 109a55f..06e449d 100644
--- a/user/car-user-lib/src/android/car/userlib/UserHalHelper.java
+++ b/user/car-user-lib/src/android/car/userlib/UserHalHelper.java
@@ -22,6 +22,8 @@
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
 import android.hardware.automotive.vehicle.V2_0.InitialUserInfoRequestType;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponse;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
 import android.hardware.automotive.vehicle.V2_0.UserFlags;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociation;
 import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue;
@@ -35,8 +37,11 @@
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.DebugUtils;
+import android.util.Log;
 
+import java.util.Arrays;
 import java.util.Objects;
 
 /**
@@ -44,8 +49,14 @@
  */
 public final class UserHalHelper {
 
+    private static final String TAG = UserHalHelper.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    public static final int INITIAL_USER_INFO_PROPERTY = 299896583;
     public static final int USER_IDENTIFICATION_ASSOCIATION_PROPERTY = 299896587;
 
+    private static final String STRING_SEPARATOR = "\\|\\|";
+
     /**
      * Gets user-friendly representation of the status.
      */
@@ -268,6 +279,8 @@
     /**
      * Creates a {@link UserIdentificationResponse} from a generic {@link VehiclePropValue} sent by
      * HAL.
+     *
+     * @throws IllegalArgumentException if the HAL property doesn't have the proper format.
      */
     @NonNull
     public static UserIdentificationResponse toUserIdentificationResponse(
@@ -276,7 +289,7 @@
         checkArgument(prop.prop == USER_IDENTIFICATION_ASSOCIATION_PROPERTY,
                 "invalid prop on %s", prop);
         // need at least 4: request_id, number associations, type1, value1
-        checkArgument(prop.value.int32Values.size() >= 4, "not enough int32Values on %s", prop);
+        assertMinimumSize(prop, 4);
 
         int requestId = prop.value.int32Values.get(0);
         checkArgument(requestId > 0, "invalid request id (%d) on %s", requestId, prop);
@@ -312,8 +325,73 @@
     }
 
     /**
+     * Creates a {@link InitialUserInfoResponse} from a generic {@link VehiclePropValue} sent by
+     * HAL.
+     *
+     * @throws IllegalArgumentException if the HAL property doesn't have the proper format.
+     */
+    @NonNull
+    public static InitialUserInfoResponse toInitialUserInfoResponse(
+            @NonNull VehiclePropValue prop) {
+        if (DEBUG) Log.d(TAG, "toInitialUserInfoResponse(): " + prop);
+        Objects.requireNonNull(prop, "prop cannot be null");
+        checkArgument(prop.prop == INITIAL_USER_INFO_PROPERTY, "invalid prop on %s", prop);
+
+        // need at least 2: request_id, action_type
+        assertMinimumSize(prop, 2);
+
+        int requestId = prop.value.int32Values.get(0);
+        checkArgument(requestId > 0, "invalid request id (%d) on %s", requestId, prop);
+
+        InitialUserInfoResponse response = new InitialUserInfoResponse();
+        response.requestId = requestId;
+        response.action = prop.value.int32Values.get(1);
+
+        String[] stringValues = null;
+        if (!TextUtils.isEmpty(prop.value.stringValue)) {
+            stringValues = TextUtils.split(prop.value.stringValue, STRING_SEPARATOR);
+            if (DEBUG) {
+                Log.d(TAG, "toInitialUserInfoResponse(): values=" + Arrays.toString(stringValues)
+                        + " length: " + stringValues.length);
+            }
+        }
+        if (stringValues != null && stringValues.length > 0) {
+            response.userLocales = stringValues[0];
+        }
+
+        switch (response.action) {
+            case InitialUserInfoResponseAction.DEFAULT:
+                response.userToSwitchOrCreate.userId = UserHandle.USER_NULL;
+                response.userToSwitchOrCreate.flags = UserFlags.NONE;
+                break;
+            case InitialUserInfoResponseAction.SWITCH:
+                assertMinimumSize(prop, 3);
+                response.userToSwitchOrCreate.userId = prop.value.int32Values.get(2);
+                response.userToSwitchOrCreate.flags = UserFlags.NONE;
+                break;
+            case InitialUserInfoResponseAction.CREATE:
+                assertMinimumSize(prop, 3);
+                response.userToSwitchOrCreate.userId = UserHandle.USER_NULL;
+                response.userToSwitchOrCreate.flags = prop.value.int32Values.get(2);
+                if (stringValues.length > 1) {
+                    response.userNameToCreate = stringValues[1];
+                }
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid response action (" + response.action + " on " + prop);
+        }
+
+        if (DEBUG) Log.d(TAG, "returning : " + response);
+
+        return response;
+    }
+
+    /**
      * Creates a generic {@link VehiclePropValue} (that can be sent to HAL) from a
      * {@link UserIdentificationGetRequest}.
+     *
+     * @throws IllegalArgumentException if the request doesn't have the proper format.
      */
     @NonNull
     public static VehiclePropValue toVehiclePropValue(
@@ -343,6 +421,8 @@
     /**
      * Creates a generic {@link VehiclePropValue} (that can be sent to HAL) from a
      * {@link UserIdentificationSetRequest}.
+     *
+     * @throws IllegalArgumentException if the request doesn't have the proper format.
      */
     @NonNull
     public static VehiclePropValue toVehiclePropValue(
@@ -372,6 +452,11 @@
         return propValue;
     }
 
+    private static void assertMinimumSize(@NonNull VehiclePropValue prop, int minSize) {
+        checkArgument(prop.value.int32Values.size() >= minSize,
+                "not enough int32Values (minimum is %d) on %s", minSize, prop);
+    }
+
     private UserHalHelper() {
         throw new UnsupportedOperationException("contains only static methods");
     }
diff --git a/watchdog/server/src/WatchdogProcessService.cpp b/watchdog/server/src/WatchdogProcessService.cpp
index 749ed30..8d4ec06 100644
--- a/watchdog/server/src/WatchdogProcessService.cpp
+++ b/watchdog/server/src/WatchdogProcessService.cpp
@@ -15,7 +15,7 @@
  */
 
 #define LOG_TAG "carwatchdogd"
-#define DEBUG true  // TODO(b/151474489): stop ship if true.
+#define DEBUG false  // STOPSHIP if true.
 
 #include "WatchdogProcessService.h"
 
diff --git a/watchdog/server/tests/LooperStub.cpp b/watchdog/server/tests/LooperStub.cpp
index 1b7aa82..bd3311b 100644
--- a/watchdog/server/tests/LooperStub.cpp
+++ b/watchdog/server/tests/LooperStub.cpp
@@ -31,8 +31,15 @@
 using android::base::Error;
 using android::base::Result;
 
-const std::chrono::milliseconds kLooperPollTimeout = 10ms;
-const std::chrono::milliseconds kStubPollCheckTimeout = 200ms;
+// As the messages, which are to be polled immediately, are enqueued in the underlying looper
+// handler before calling its poll method, the looper handler doesn't have to wait for any new
+// messages.
+const std::chrono::milliseconds kLooperPollTimeout = 0ms;
+
+// Maximum timeout before giving up on the underlying looper handler. This doesn't block the test
+// as long as the underlying looper handler processes the enqueued messages quickly and updates
+// |mShouldPoll|.
+const std::chrono::milliseconds kStubPollCheckTimeout = 3min;
 
 int LooperStub::pollAll(int /*timeoutMillis*/) {
     {