Merge "Implemented CarUserManager.setUserIdentificationAssociation()" 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 fb368e6..e082df8 100644
--- a/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
+++ b/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
@@ -76,6 +76,8 @@
 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)
@@ -96,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 7daf63b..f2d5cb8 100644
--- a/car-lib/src/android/car/user/CarUserManager.java
+++ b/car-lib/src/android/car/user/CarUserManager.java
@@ -54,6 +54,7 @@
 
 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;
@@ -70,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
@@ -204,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>() {
@@ -317,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,
@@ -337,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
@@ -434,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/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..e0f7b98 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;
@@ -929,20 +930,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 +942,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 +1009,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 +1036,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 +1081,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 +1142,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 +1150,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) {
diff --git a/service/src/com/android/car/user/CarUserService.java b/service/src/com/android/car/user/CarUserService.java
index 158f4a0..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);
+        });
     }
 
     /**
@@ -1006,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 {
@@ -1018,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/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java
index 5a93b8c..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
@@ -28,6 +28,7 @@
 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;
@@ -44,7 +45,7 @@
 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;
@@ -234,19 +235,22 @@
 
     @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);
@@ -259,6 +263,80 @@
         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 bea777c..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);
@@ -1123,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!");
     }
 
@@ -1185,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);
@@ -1194,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)
@@ -1296,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();
+    }
+}