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();
+ }
+}