Merge "Cleaning checklyst issues." into rvc-dev
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..e082df8 100644
--- a/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
+++ b/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
@@ -75,6 +75,9 @@
 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)
@@ -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/ICarUserService.aidl b/car-lib/src/android/car/ICarUserService.aidl
index 2eccdbb..71227aa 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;
@@ -36,6 +36,8 @@
     oneway void setLifecycleListenerForUid(in IResultReceiver listener);
     oneway void resetLifecycleListenerForUid();
     oneway void getInitialUserInfo(int requestType, int timeoutMs, in IResultReceiver receiver);
-    GetUserIdentificationAssociationResponse getUserIdentificationAssociation(in int[] types);
+    UserIdentificationAssociationResponse getUserIdentificationAssociation(in int[] types);
+    void setUserIdentificationAssociation(int timeoutMs, in int[] types, in int[] values,
+      in AndroidFuture<UserIdentificationAssociationResponse> result);
     oneway void setUserSwitchUiCallback(in IResultReceiver callback);
 }
diff --git a/car-lib/src/android/car/user/CarUserManager.java b/car-lib/src/android/car/user/CarUserManager.java
index 784ee48..f2d5cb8 100644
--- a/car-lib/src/android/car/user/CarUserManager.java
+++ b/car-lib/src/android/car/user/CarUserManager.java
@@ -54,7 +54,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 +71,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
@@ -203,6 +204,7 @@
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
     public AndroidFuture<UserSwitchResult> switchUser(@UserIdInt int targetUserId) {
+        // TODO(b/155311595): add permission check integration test
         int uid = myUid();
         try {
             AndroidFuture<UserSwitchResult> future = new AndroidFuture<UserSwitchResult>() {
@@ -231,6 +233,8 @@
     /**
      * Adds a listener for {@link UserLifecycleEvent user lifecycle events}.
      *
+     * @throws IllegalStateException if the listener was already added.
+     *
      * @hide
      */
     @SystemApi
@@ -238,15 +242,14 @@
     @RequiresPermission(anyOf = {INTERACT_ACROSS_USERS, INTERACT_ACROSS_USERS_FULL})
     public void addListener(@NonNull @CallbackExecutor Executor executor,
             @NonNull UserLifecycleListener listener) {
+        Objects.requireNonNull(executor, "executor cannot be null");
+        Objects.requireNonNull(listener, "listener cannot be null");
         checkInteractAcrossUsersPermission();
 
-        // TODO(b/144120654): add unit tests to validate input
-        // - executor cannot be null
-        // - listener cannot be null
-        // - listener must not be added before
-
         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 +274,21 @@
     /**
      * 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) {
+        Objects.requireNonNull(listener, "listener cannot be null");
         checkInteractAcrossUsersPermission();
 
-        // 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 +318,15 @@
      *
      * @hide
      */
-    @Nullable
+    @NonNull
     @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
-    public GetUserIdentificationAssociationResponse getUserIdentificationAssociation(
+    public UserIdentificationAssociationResponse getUserIdentificationAssociation(
             @NonNull int... types) {
+        // TODO(b/155311595): add permission check integration test
         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 +339,60 @@
     }
 
     /**
+     * Sets the user authentication types associated with this manager's user.
+     *
+     * @hide
+     */
+    @NonNull
+    public AndroidFuture<UserIdentificationAssociationResponse> setUserIdentificationAssociation(
+            @NonNull int[] types, @NonNull int[] values) {
+        // TODO(b/155311595): add permission check integration test
+        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
@@ -435,6 +490,8 @@
         checkInteractAcrossUsersPermission(getContext());
     }
 
+    // TODO(b/155311595): remove once permission check is tested by integration tests (as the
+    // permission is also checked on service side, and that's enough)
     private static void checkInteractAcrossUsersPermission(Context context) {
         if (context.checkSelfPermission(INTERACT_ACROSS_USERS) != PERMISSION_GRANTED
                 && context.checkSelfPermission(INTERACT_ACROSS_USERS_FULL) != PERMISSION_GRANTED) {
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-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java b/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
index e5c5740..ce96060 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;
@@ -92,23 +94,44 @@
     private MockitoSession mSession;
     private MockSettings mSettings;
 
+    @Nullable
+    private final TimingsTraceLog mTracer;
+
     @Rule
     public final WtfCheckerRule mWtfCheckerRule = new WtfCheckerRule();
 
+    protected AbstractExtendedMockitoTestCase() {
+        mTracer = VERBOSE ? 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();
         }
+        endTrace();
     }
 
     /**
@@ -172,7 +195,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 +212,42 @@
     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,6 +303,9 @@
                     .spyStatic(Slog.class);
 
         onSessionBuilder(customBuilder);
+
+        if (VERBOSE) Log.v(TAG, "spied classes" + customBuilder.mStaticSpiedClasses);
+
         return builder.initMocks(this);
     }
 
@@ -295,20 +356,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();
                     }
                 }
             };
@@ -386,14 +453,14 @@
             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 +468,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/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/service/src/com/android/car/CarShellCommand.java b/service/src/com/android/car/CarShellCommand.java
index 871e18c..fc6b2d2 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,7 +34,7 @@
 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;
@@ -286,8 +287,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");
@@ -439,9 +443,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 +456,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 +464,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) {
@@ -929,20 +936,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 +948,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 +1015,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 +1042,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 +1087,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 +1148,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 +1156,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 +1246,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 +1261,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/hal/UserHalService.java b/service/src/com/android/car/hal/UserHalService.java
index f995047..ce43be4 100644
--- a/service/src/com/android/car/hal/UserHalService.java
+++ b/service/src/com/android/car/hal/UserHalService.java
@@ -53,6 +53,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 +97,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 +112,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 +344,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 +612,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 +621,7 @@
     }
 
     @Nullable
-    private PendingRequest<?, ?> getPendingResponse(int requestId) {
+    private PendingRequest<?, ?> getPendingRequest(int requestId) {
         synchronized (mLock) {
             return mPendingRequests.get(requestId);
         }
@@ -666,6 +674,52 @@
 
     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 +732,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 +750,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..00502d1 100644
--- a/service/src/com/android/car/user/CarUserService.java
+++ b/service/src/com/android/car/user/CarUserService.java
@@ -33,7 +33,7 @@
 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;
@@ -48,6 +48,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 +62,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;
@@ -867,7 +870,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 +891,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 +900,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 +995,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();
@@ -963,7 +1070,7 @@
         mUserSwitchUiReceiver = receiver;
     }
 
-    // TODO(b/144120654): use helper to generate UsersInfo
+    // TODO(b/150413515): use helper to generate UsersInfo
     private UsersInfo getUsersInfo() {
         UserInfo currentUser;
         try {
@@ -975,7 +1082,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();
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/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/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
index 033bb9a..67a9b6f 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
@@ -464,7 +483,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 +595,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 +1100,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/user/CarUserManagerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java
index abfd48d..80318a7 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,9 +15,11 @@
  */
 package com.android.car.user;
 
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUsers;
 import static android.car.test.util.UserTestingHelper.newUsers;
 import static android.car.testapi.CarMockitoHelper.mockHandleRemoteExceptionFromCarServiceWithDefaultValue;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.UserHandle.USER_SYSTEM;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -26,8 +28,10 @@
 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.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
@@ -39,9 +43,11 @@
 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.Context;
 import android.content.pm.UserInfo;
 import android.os.RemoteException;
 import android.os.UserManager;
@@ -117,9 +123,68 @@
     }
 
     @Test
+    public void testAddListener_nullExecutor() {
+        mockInteractAcrossUsersPermission();
+
+        assertThrows(NullPointerException.class, () -> mMgr.addListener(null, (e) -> { }));
+    }
+
+    @Test
+    public void testAddListener_nullListener() {
+        mockInteractAcrossUsersPermission();
+
+        assertThrows(NullPointerException.class, () -> mMgr.addListener(Runnable::run, null));
+    }
+
+    @Test
+    public void testAddListener_sameListenerAddedTwice() {
+        mockInteractAcrossUsersPermission();
+
+        UserLifecycleListener listener = (e) -> { };
+
+        mMgr.addListener(Runnable::run, listener);
+        assertThrows(IllegalStateException.class, () -> mMgr.addListener(Runnable::run, listener));
+    }
+
+    @Test
+    public void testAddListener_differentListenersAddedTwice() {
+        mockInteractAcrossUsersPermission();
+
+        mMgr.addListener(Runnable::run, (e) -> { });
+        mMgr.addListener(Runnable::run, (e) -> { });
+    }
+
+    @Test
+    public void testRemoveListener_nullListener() {
+        mockInteractAcrossUsersPermission();
+
+        assertThrows(NullPointerException.class, () -> mMgr.removeListener(null));
+    }
+
+    @Test
+    public void testRemoveListener_notAddedBefore() {
+        mockInteractAcrossUsersPermission();
+
+        UserLifecycleListener listener = (e) -> { };
+
+        assertThrows(IllegalStateException.class, () -> mMgr.removeListener(listener));
+    }
+
+    @Test
+    public void testRemoveListener_addAndRemove() {
+        mockInteractAcrossUsersPermission();
+        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 +235,108 @@
 
     @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);
     }
 
+    // TODO(b/155311595): remove once permission check is done only on service
+    private void mockInteractAcrossUsersPermission() {
+        Context context = mock(Context.class);
+        when(mCar.getContext()).thenReturn(context);
+        when(context.checkSelfPermission(INTERACT_ACROSS_USERS)).thenReturn(PERMISSION_GRANTED);
+    }
+
+    @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 {
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..f031686 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
@@ -61,7 +61,7 @@
 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;
@@ -79,6 +79,7 @@
 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.Bundle;
@@ -153,6 +154,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);
@@ -953,6 +956,30 @@
     }
 
     @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));
@@ -1099,39 +1126,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,6 +1282,12 @@
     }
 
     @NonNull
+    private UserIdentificationAssociationResponse getUserAssociationRespResult()
+            throws Exception {
+        return getResult(mUserAssociationRespFuture);
+    }
+
+    @NonNull
     private <T> T getResult(@NonNull AndroidFuture<T> future) throws Exception {
         try {
             return future.get(mAsyncCallTimeoutMs, TimeUnit.MILLISECONDS);
@@ -1170,6 +1297,19 @@
     }
 
     /**
+     * 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;
+    }
+
+    /**
      * Mock calls that generate a {@code UsersInfo}.
      */
     private void mockExistingUsersAndCurrentUser(@NonNull UserInfo user)
@@ -1272,6 +1412,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) {
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();
+    }
+}