Merge "Cleaning checklyst issues." into rvc-dev
diff --git a/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags b/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
index 60a174d..e082df8 100644
--- a/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
+++ b/car-internal-lib/src/com/android/internal/car/EventLogTags.logtags
@@ -75,6 +75,9 @@
150109 car_user_svc_get_user_auth_req (uid|1),(user_id|1),(number_types|1)
150110 car_user_svc_get_user_auth_resp (number_values|1)
150111 car_user_svc_switch_user_ui_req (user_id|1)
+150112 car_user_svc_switch_user_from_hal_req (request_id|1),(uid|1)
+150113 car_user_svc_set_user_auth_req (uid|1),(user_id|1),(number_associations|1)
+150114 car_user_svc_set_user_auth_resp (number_values|1),(error_message|3)
150140 car_user_hal_initial_user_info_req (request_id|1),(request_type|1),(timeout|1)
150141 car_user_hal_initial_user_info_resp (request_id|1),(status|1),(action|1),(user_id|1),(flags|1),(safe_name|3)
@@ -86,6 +89,7 @@
150147 car_user_hal_legacy_switch_user_req (request_id|1),(target_user_id|1),(current_user_id|1)
150148 car_user_hal_set_user_auth_req (int32values|4)
150149 car_user_hal_set_user_auth_resp (int32values|4),(error_message|3)
+150150 car_user_hal_oem_switch_user_req (request_id|1),(target_user_id|1)
150171 car_user_mgr_add_listener (uid|1)
150172 car_user_mgr_remove_listener (uid|1)
@@ -94,3 +98,5 @@
150175 car_user_mgr_switch_user_response (uid|1),(status|1),(error_message|3)
150176 car_user_mgr_get_user_auth_req (types|4)
150177 car_user_mgr_get_user_auth_resp (values|4)
+150178 car_user_mgr_set_user_auth_req (types_and_values_pairs|4)
+150179 car_user_mgr_set_user_auth_resp (values|4)
diff --git a/car-lib/src/android/car/ICarUserService.aidl b/car-lib/src/android/car/ICarUserService.aidl
index 2eccdbb..71227aa 100644
--- a/car-lib/src/android/car/ICarUserService.aidl
+++ b/car-lib/src/android/car/ICarUserService.aidl
@@ -17,7 +17,7 @@
package android.car;
import android.content.pm.UserInfo;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
import android.car.user.UserSwitchResult;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.os.IResultReceiver;
@@ -36,6 +36,8 @@
oneway void setLifecycleListenerForUid(in IResultReceiver listener);
oneway void resetLifecycleListenerForUid();
oneway void getInitialUserInfo(int requestType, int timeoutMs, in IResultReceiver receiver);
- GetUserIdentificationAssociationResponse getUserIdentificationAssociation(in int[] types);
+ UserIdentificationAssociationResponse getUserIdentificationAssociation(in int[] types);
+ void setUserIdentificationAssociation(int timeoutMs, in int[] types, in int[] values,
+ in AndroidFuture<UserIdentificationAssociationResponse> result);
oneway void setUserSwitchUiCallback(in IResultReceiver callback);
}
diff --git a/car-lib/src/android/car/user/CarUserManager.java b/car-lib/src/android/car/user/CarUserManager.java
index 784ee48..f2d5cb8 100644
--- a/car-lib/src/android/car/user/CarUserManager.java
+++ b/car-lib/src/android/car/user/CarUserManager.java
@@ -54,7 +54,9 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -69,8 +71,7 @@
private static final String TAG = CarUserManager.class.getSimpleName();
private static final int HAL_TIMEOUT_MS = CarProperties.user_hal_timeout().orElse(5_000);
- // TODO(b/144120654): STOPSHIP - set to false
- private static final boolean DBG = true;
+ private static final boolean DBG = false;
/**
* {@link UserLifecycleEvent} called when the user is starting, for components to initialize
@@ -203,6 +204,7 @@
*/
@RequiresPermission(android.Manifest.permission.MANAGE_USERS)
public AndroidFuture<UserSwitchResult> switchUser(@UserIdInt int targetUserId) {
+ // TODO(b/155311595): add permission check integration test
int uid = myUid();
try {
AndroidFuture<UserSwitchResult> future = new AndroidFuture<UserSwitchResult>() {
@@ -231,6 +233,8 @@
/**
* Adds a listener for {@link UserLifecycleEvent user lifecycle events}.
*
+ * @throws IllegalStateException if the listener was already added.
+ *
* @hide
*/
@SystemApi
@@ -238,15 +242,14 @@
@RequiresPermission(anyOf = {INTERACT_ACROSS_USERS, INTERACT_ACROSS_USERS_FULL})
public void addListener(@NonNull @CallbackExecutor Executor executor,
@NonNull UserLifecycleListener listener) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(listener, "listener cannot be null");
checkInteractAcrossUsersPermission();
- // TODO(b/144120654): add unit tests to validate input
- // - executor cannot be null
- // - listener cannot be null
- // - listener must not be added before
-
int uid = myUid();
synchronized (mLock) {
+ Preconditions.checkState(mListeners == null || !mListeners.containsKey(listener),
+ "already called for this listener");
if (mReceiver == null) {
mReceiver = new LifecycleResultReceiver();
try {
@@ -271,24 +274,21 @@
/**
* Removes a listener for {@link UserLifecycleEvent user lifecycle events}.
*
+ * @throws IllegalStateException if the listener was not added beforehand.
+ *
* @hide
*/
@SystemApi
@TestApi
@RequiresPermission(anyOf = {INTERACT_ACROSS_USERS, INTERACT_ACROSS_USERS_FULL})
public void removeListener(@NonNull UserLifecycleListener listener) {
+ Objects.requireNonNull(listener, "listener cannot be null");
checkInteractAcrossUsersPermission();
- // TODO(b/144120654): add unit tests to validate input
- // - listener cannot be null
- // - listener must not be added before
int uid = myUid();
synchronized (mLock) {
- if (mListeners == null) {
- Log.w(TAG, "removeListener(): no listeners for uid " + uid);
- return;
- }
-
+ Preconditions.checkState(mListeners != null && mListeners.containsKey(listener),
+ "not called for this listener yet");
mListeners.remove(listener);
if (!mListeners.isEmpty()) {
@@ -318,14 +318,15 @@
*
* @hide
*/
- @Nullable
+ @NonNull
@RequiresPermission(android.Manifest.permission.MANAGE_USERS)
- public GetUserIdentificationAssociationResponse getUserIdentificationAssociation(
+ public UserIdentificationAssociationResponse getUserIdentificationAssociation(
@NonNull int... types) {
+ // TODO(b/155311595): add permission check integration test
Preconditions.checkArgument(!ArrayUtils.isEmpty(types), "must have at least one type");
EventLog.writeEvent(EventLogTags.CAR_USER_MGR_GET_USER_AUTH_REQ, types.length);
try {
- GetUserIdentificationAssociationResponse response =
+ UserIdentificationAssociationResponse response =
mService.getUserIdentificationAssociation(types);
if (response != null) {
EventLog.writeEvent(EventLogTags.CAR_USER_MGR_GET_USER_AUTH_RESP,
@@ -338,6 +339,60 @@
}
/**
+ * Sets the user authentication types associated with this manager's user.
+ *
+ * @hide
+ */
+ @NonNull
+ public AndroidFuture<UserIdentificationAssociationResponse> setUserIdentificationAssociation(
+ @NonNull int[] types, @NonNull int[] values) {
+ // TODO(b/155311595): add permission check integration test
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(types), "must have at least one type");
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(values), "must have at least one value");
+ if (types.length != values.length) {
+ throw new IllegalArgumentException("types (" + Arrays.toString(types) + ") and values ("
+ + Arrays.toString(values) + ") should have the same length");
+ }
+ // TODO(b/153900032): move this logic to a common helper
+ Object[] loggedValues = new Integer[types.length * 2];
+ for (int i = 0; i < types.length; i++) {
+ loggedValues[i * 2] = types[i];
+ loggedValues[i * 2 + 1 ] = values[i];
+ }
+ EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_REQ, loggedValues);
+
+ try {
+ AndroidFuture<UserIdentificationAssociationResponse> future =
+ new AndroidFuture<UserIdentificationAssociationResponse>() {
+ @Override
+ protected void onCompleted(UserIdentificationAssociationResponse result,
+ Throwable err) {
+ if (result != null) {
+ int[] rawValues = result.getValues();
+ // TODO(b/153900032): move this logic to a common helper
+ Object[] loggedValues = new Object[rawValues.length];
+ for (int i = 0; i < rawValues.length; i++) {
+ loggedValues[i] = rawValues[i];
+ }
+ EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_RESP,
+ loggedValues);
+ } else {
+ Log.w(TAG, "setUserIdentificationAssociation(" + Arrays.toString(types)
+ + ", " + Arrays.toString(values) + ") failed: " + err);
+ }
+ super.onCompleted(result, err);
+ };
+ };
+ mService.setUserIdentificationAssociation(HAL_TIMEOUT_MS, types, values, future);
+ return future;
+ } catch (RemoteException e) {
+ AndroidFuture<UserIdentificationAssociationResponse> future = new AndroidFuture<>();
+ future.complete(UserIdentificationAssociationResponse.forFailure());
+ return handleRemoteExceptionFromCarService(e, future);
+ }
+ }
+
+ /**
* Sets a callback to be notified before user switch. It should only be used by Car System UI.
*
* @hide
@@ -435,6 +490,8 @@
checkInteractAcrossUsersPermission(getContext());
}
+ // TODO(b/155311595): remove once permission check is tested by integration tests (as the
+ // permission is also checked on service side, and that's enough)
private static void checkInteractAcrossUsersPermission(Context context) {
if (context.checkSelfPermission(INTERACT_ACROSS_USERS) != PERMISSION_GRANTED
&& context.checkSelfPermission(INTERACT_ACROSS_USERS_FULL) != PERMISSION_GRANTED) {
diff --git a/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java b/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java
deleted file mode 100644
index 12bee07..0000000
--- a/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.car.user;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.os.Parcelable;
-
-import com.android.internal.util.DataClass;
-
-/**
- * Results of a {@link CarUserManager#getUserIdentificationAssociation(int[]) request.
- *
- * @hide
- */
-@DataClass(
- genToString = true,
- genHiddenConstructor = true,
- genHiddenConstDefs = true)
-public final class GetUserIdentificationAssociationResponse implements Parcelable {
-
- /**
- * Gets the error message returned by the HAL.
- */
- @Nullable
- private String mErrorMessage;
-
- /**
- * Gets the list of
- * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}
- * associates with the request.
- */
- @NonNull
- private final int[] mValues;
-
-
-
-
- // Code below generated by codegen v1.0.15.
- //
- // DO NOT MODIFY!
- // CHECKSTYLE:OFF Generated code
- //
- // To regenerate run:
- // $ codegen $ANDROID_BUILD_TOP/packages/services/Car/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java
- //
- // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
- // Settings > Editor > Code Style > Formatter Control
- //@formatter:off
-
-
- /**
- * Creates a new GetUserIdentificationAssociationResponse.
- *
- * @param errorMessage
- * Gets the error message returned by the HAL.
- * @param values
- * Gets the list of
- * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}
- * associates with the request.
- * @hide
- */
- @DataClass.Generated.Member
- public GetUserIdentificationAssociationResponse(
- @Nullable String errorMessage,
- @NonNull int[] values) {
- this.mErrorMessage = errorMessage;
- this.mValues = values;
- com.android.internal.util.AnnotationValidations.validate(
- NonNull.class, null, mValues);
-
- // onConstructed(); // You can define this method to get a callback
- }
-
- /**
- * Gets the error message returned by the HAL.
- */
- @DataClass.Generated.Member
- public @Nullable String getErrorMessage() {
- return mErrorMessage;
- }
-
- /**
- * Gets the list of
- * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}
- * associates with the request.
- */
- @DataClass.Generated.Member
- public @NonNull int[] getValues() {
- return mValues;
- }
-
- @Override
- @DataClass.Generated.Member
- public String toString() {
- // You can override field toString logic by defining methods like:
- // String fieldNameToString() { ... }
-
- return "GetUserIdentificationAssociationResponse { " +
- "errorMessage = " + mErrorMessage + ", " +
- "values = " + java.util.Arrays.toString(mValues) +
- " }";
- }
-
- @Override
- @DataClass.Generated.Member
- public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
- // You can override field parcelling by defining methods like:
- // void parcelFieldName(Parcel dest, int flags) { ... }
-
- byte flg = 0;
- if (mErrorMessage != null) flg |= 0x1;
- dest.writeByte(flg);
- if (mErrorMessage != null) dest.writeString(mErrorMessage);
- dest.writeIntArray(mValues);
- }
-
- @Override
- @DataClass.Generated.Member
- public int describeContents() { return 0; }
-
- /** @hide */
- @SuppressWarnings({"unchecked", "RedundantCast"})
- @DataClass.Generated.Member
- /* package-private */ GetUserIdentificationAssociationResponse(@NonNull android.os.Parcel in) {
- // You can override field unparcelling by defining methods like:
- // static FieldType unparcelFieldName(Parcel in) { ... }
-
- byte flg = in.readByte();
- String errorMessage = (flg & 0x1) == 0 ? null : in.readString();
- int[] values = in.createIntArray();
-
- this.mErrorMessage = errorMessage;
- this.mValues = values;
- com.android.internal.util.AnnotationValidations.validate(
- NonNull.class, null, mValues);
-
- // onConstructed(); // You can define this method to get a callback
- }
-
- @DataClass.Generated.Member
- public static final @NonNull Parcelable.Creator<GetUserIdentificationAssociationResponse> CREATOR
- = new Parcelable.Creator<GetUserIdentificationAssociationResponse>() {
- @Override
- public GetUserIdentificationAssociationResponse[] newArray(int size) {
- return new GetUserIdentificationAssociationResponse[size];
- }
-
- @Override
- public GetUserIdentificationAssociationResponse createFromParcel(@NonNull android.os.Parcel in) {
- return new GetUserIdentificationAssociationResponse(in);
- }
- };
-
- @DataClass.Generated(
- time = 1587769987549L,
- codegenVersion = "1.0.15",
- sourceFile = "packages/services/Car/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.java",
- inputSignatures = "private @android.annotation.Nullable java.lang.String mErrorMessage\nprivate final @android.annotation.NonNull int[] mValues\nclass GetUserIdentificationAssociationResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
- @Deprecated
- private void __metadata() {}
-
-
- //@formatter:on
- // End of generated code
-
-}
diff --git a/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.aidl b/car-lib/src/android/car/user/UserIdentificationAssociationResponse.aidl
similarity index 92%
rename from car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.aidl
rename to car-lib/src/android/car/user/UserIdentificationAssociationResponse.aidl
index d8ac0dc..9e292d4 100644
--- a/car-lib/src/android/car/user/GetUserIdentificationAssociationResponse.aidl
+++ b/car-lib/src/android/car/user/UserIdentificationAssociationResponse.aidl
@@ -16,4 +16,4 @@
package android.car.user;
-parcelable GetUserIdentificationAssociationResponse;
+parcelable UserIdentificationAssociationResponse;
diff --git a/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java b/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java
new file mode 100644
index 0000000..d2cdb44
--- /dev/null
+++ b/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.car.user;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcelable;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.DataClass;
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Results of a {@link CarUserManager#getUserIdentificationAssociation(int[]) request.
+ *
+ * @hide
+ */
+@DataClass(
+ genToString = true,
+ genHiddenConstructor = false,
+ genHiddenConstDefs = true)
+public final class UserIdentificationAssociationResponse implements Parcelable {
+
+ /**
+ * Whether the request was successful.
+ *
+ * <p>A successful option has non-null {@link #getValues()}
+ */
+ private final boolean mSuccess;
+
+ /**
+ * Gets the error message returned by the HAL.
+ */
+ @Nullable
+ private final String mErrorMessage;
+
+ /**
+ * Gets the list of values associated with the request.
+ *
+ * <p><b>NOTE: </b>It's only set when the response is {@link #isSuccess() successful}.
+ *
+ * <p>For {@link CarUserManager#getUserIdentificationAssociation(int...)}, the values are
+ * defined on
+ * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}.
+ *
+ * <p>For {@link CarUserManager#setUserIdentificationAssociation(int...)}, the values are
+ * defined on
+ * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue}.
+ */
+ @Nullable
+ private final int[] mValues;
+
+ private UserIdentificationAssociationResponse(
+ boolean success,
+ @Nullable String errorMessage,
+ @Nullable int[] values) {
+ this.mSuccess = success;
+ this.mErrorMessage = errorMessage;
+ this.mValues = values;
+ }
+
+ /**
+ * Factory method for failed UserIdentificationAssociationResponse requests.
+ */
+ @NonNull
+ public static UserIdentificationAssociationResponse forFailure() {
+ return forFailure(/* errorMessage= */ null);
+ }
+
+ /**
+ * Factory method for failed UserIdentificationAssociationResponse requests.
+ */
+ @NonNull
+ public static UserIdentificationAssociationResponse forFailure(@Nullable String errorMessage) {
+ return new UserIdentificationAssociationResponse(/* success= */ false,
+ errorMessage, /* values= */ null);
+ }
+
+ /**
+ * Factory method for successful UserIdentificationAssociationResponse requests.
+ */
+ @NonNull
+ public static UserIdentificationAssociationResponse forSuccess(@NonNull int[] values) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(values), "must have at least one value");
+ return new UserIdentificationAssociationResponse(/* success= */ true,
+ /* errorMessage= */ null, Objects.requireNonNull(values));
+ }
+
+ /**
+ * Factory method for successful UserIdentificationAssociationResponse requests.
+ */
+ @NonNull
+ public static UserIdentificationAssociationResponse forSuccess(@NonNull int[] values,
+ @Nullable String errorMessage) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(values), "must have at least one value");
+ return new UserIdentificationAssociationResponse(/* success= */ true,
+ errorMessage, Objects.requireNonNull(values));
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/packages/services/Car/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Whether the request was successful.
+ *
+ * <p>A successful option has non-null {@link #getValues()}
+ */
+ @DataClass.Generated.Member
+ public boolean isSuccess() {
+ return mSuccess;
+ }
+
+ /**
+ * Gets the error message returned by the HAL.
+ */
+ @DataClass.Generated.Member
+ public @Nullable String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /**
+ * Gets the list of values associated with the request.
+ *
+ * <p><b>NOTE: </b>It's only set when the response is {@link #isSuccess() successful}.
+ *
+ * <p>For {@link CarUserManager#getUserIdentificationAssociation(int...)}, the values are
+ * defined on
+ * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationValue}.
+ *
+ * <p>For {@link CarUserManager#setUserIdentificationAssociation(int...)}, the values are
+ * defined on
+ * {@link android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue}.
+ */
+ @DataClass.Generated.Member
+ public @Nullable int[] getValues() {
+ return mValues;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "UserIdentificationAssociationResponse { " +
+ "success = " + mSuccess + ", " +
+ "errorMessage = " + mErrorMessage + ", " +
+ "values = " + java.util.Arrays.toString(mValues) +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mSuccess) flg |= 0x1;
+ if (mErrorMessage != null) flg |= 0x2;
+ if (mValues != null) flg |= 0x4;
+ dest.writeByte(flg);
+ if (mErrorMessage != null) dest.writeString(mErrorMessage);
+ if (mValues != null) dest.writeIntArray(mValues);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ UserIdentificationAssociationResponse(@NonNull android.os.Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ boolean success = (flg & 0x1) != 0;
+ String errorMessage = (flg & 0x2) == 0 ? null : in.readString();
+ int[] values = (flg & 0x4) == 0 ? null : in.createIntArray();
+
+ this.mSuccess = success;
+ this.mErrorMessage = errorMessage;
+ this.mValues = values;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<UserIdentificationAssociationResponse> CREATOR
+ = new Parcelable.Creator<UserIdentificationAssociationResponse>() {
+ @Override
+ public UserIdentificationAssociationResponse[] newArray(int size) {
+ return new UserIdentificationAssociationResponse[size];
+ }
+
+ @Override
+ public UserIdentificationAssociationResponse createFromParcel(@NonNull android.os.Parcel in) {
+ return new UserIdentificationAssociationResponse(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1588982917194L,
+ codegenVersion = "1.0.15",
+ sourceFile = "packages/services/Car/car-lib/src/android/car/user/UserIdentificationAssociationResponse.java",
+ inputSignatures = "private final boolean mSuccess\nprivate final @android.annotation.Nullable java.lang.String mErrorMessage\nprivate final @android.annotation.Nullable int[] mValues\npublic static @android.annotation.NonNull android.car.user.UserIdentificationAssociationResponse forFailure()\npublic static @android.annotation.NonNull android.car.user.UserIdentificationAssociationResponse forFailure(java.lang.String)\npublic static @android.annotation.NonNull android.car.user.UserIdentificationAssociationResponse forSuccess(int[])\npublic static @android.annotation.NonNull android.car.user.UserIdentificationAssociationResponse forSuccess(int[],java.lang.String)\nclass UserIdentificationAssociationResponse extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=false, genHiddenConstDefs=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java b/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
index e5c5740..ce96060 100644
--- a/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
+++ b/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
@@ -31,10 +31,12 @@
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
+import android.os.Trace;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Log;
import android.util.Slog;
+import android.util.TimingsTraceLog;
import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
import com.android.internal.util.Preconditions;
@@ -92,23 +94,44 @@
private MockitoSession mSession;
private MockSettings mSettings;
+ @Nullable
+ private final TimingsTraceLog mTracer;
+
@Rule
public final WtfCheckerRule mWtfCheckerRule = new WtfCheckerRule();
+ protected AbstractExtendedMockitoTestCase() {
+ mTracer = VERBOSE ? new TimingsTraceLog(TAG, Trace.TRACE_TAG_APP) : null;
+ }
+
@Before
public final void startSession() {
- if (VERBOSE) Log.v(TAG, getLogPrefix() + "startSession()");
+ beginTrace("startSession()");
+
+ beginTrace("startMocking()");
mSession = newSessionBuilder().startMocking();
+ endTrace();
+
+ beginTrace("MockSettings()");
mSettings = new MockSettings();
+ endTrace();
+
+ beginTrace("interceptWtfCalls()");
interceptWtfCalls();
+ endTrace();
+
+ endTrace(); // startSession
}
@After
public final void finishSession() {
- if (VERBOSE) Log.v(TAG, getLogPrefix() + "finishSession()");
+ beginTrace("finishSession()");
if (mSession != null) {
+ beginTrace("finishMocking()");
mSession.finishMocking();
+ endTrace();
}
+ endTrace();
}
/**
@@ -172,7 +195,10 @@
protected final void mockGetCurrentUser(@UserIdInt int userId) {
if (VERBOSE) Log.v(TAG, getLogPrefix() + "mockGetCurrentUser(" + userId + ")");
assertSpied(ActivityManager.class);
+
+ beginTrace("mockAmGetCurrentUser-" + userId);
AndroidMockitoHelper.mockAmGetCurrentUser(userId);
+ endTrace();
}
/**
@@ -186,10 +212,42 @@
protected final void mockIsHeadlessSystemUserMode(boolean mode) {
if (VERBOSE) Log.v(TAG, getLogPrefix() + "mockIsHeadlessSystemUserMode(" + mode + ")");
assertSpied(UserManager.class);
+
+ beginTrace("mockUmIsHeadlessSystemUserMode");
AndroidMockitoHelper.mockUmIsHeadlessSystemUserMode(mode);
+ endTrace();
}
- protected void interceptWtfCalls() {
+ /**
+ * Starts a tracing message.
+ *
+ * <p>MUST be followed by a {@link #endTrace()} calls.
+ *
+ * <p>Ignored if {@value #VERBOSE} is {@code false}.
+ */
+ protected final void beginTrace(@NonNull String message) {
+ if (mTracer == null) return;
+
+ Log.d(TAG, getLogPrefix() + message);
+ mTracer.traceBegin(message);
+ }
+
+ /**
+ * Ends a tracing call.
+ *
+ * <p>MUST be called after {@link #beginTrace(String)}.
+ *
+ * <p>Ignored if {@value #VERBOSE} is {@code false}.
+ */
+ protected final void endTrace() {
+ if (mTracer == null) return;
+
+ mTracer.traceEnd();
+ }
+
+
+
+ private void interceptWtfCalls() {
doAnswer((invocation) -> {
return addWtf(invocation);
}).when(() -> Log.wtf(anyString(), anyString()));
@@ -245,6 +303,9 @@
.spyStatic(Slog.class);
onSessionBuilder(customBuilder);
+
+ if (VERBOSE) Log.v(TAG, "spied classes" + customBuilder.mStaticSpiedClasses);
+
return builder.initMocks(this);
}
@@ -295,20 +356,26 @@
@Override
public void evaluate() throws Throwable {
String testName = description.getMethodName();
-
if (VERBOSE) Log.v(TAG, "running " + testName);
+ beginTrace("evaluate-" + testName);
base.evaluate();
+ endTrace();
Method testMethod = AbstractExtendedMockitoTestCase.this.getClass()
.getMethod(testName);
ExpectWtf expectWtfAnnotation = testMethod.getAnnotation(ExpectWtf.class);
- if (expectWtfAnnotation != null) {
- if (VERBOSE) Log.v(TAG, "expecting wtf()");
- verifyWtfLogged();
- } else {
- if (VERBOSE) Log.v(TAG, "NOT expecting wtf()");
- verifyWtfNeverLogged();
+ beginTrace("verify-wtfs");
+ try {
+ if (expectWtfAnnotation != null) {
+ if (VERBOSE) Log.v(TAG, "expecting wtf()");
+ verifyWtfLogged();
+ } else {
+ if (VERBOSE) Log.v(TAG, "NOT expecting wtf()");
+ verifyWtfNeverLogged();
+ }
+ } finally {
+ endTrace();
}
}
};
@@ -386,14 +453,14 @@
return safeCast(value, clazz);
}
- private <T> T safeCast(Object value, Class<T> clazz) {
+ private static <T> T safeCast(Object value, Class<T> clazz) {
if (value == null) {
return null;
}
Preconditions.checkArgument(value.getClass() == clazz,
"Setting value has class %s but requires class %s",
value.getClass(), clazz);
- return (T) value;
+ return clazz.cast(value);
}
private String getString(String key) {
@@ -401,7 +468,7 @@
}
public int getInt(String key) {
- return (int) get(key, null, Integer.class);
+ return get(key, null, Integer.class);
}
}
diff --git a/car-test-lib/src/android/car/testapi/CarMockitoHelper.java b/car-test-lib/src/android/car/testapi/CarMockitoHelper.java
index d69a0f8..13c5361 100644
--- a/car-test-lib/src/android/car/testapi/CarMockitoHelper.java
+++ b/car-test-lib/src/android/car/testapi/CarMockitoHelper.java
@@ -22,12 +22,15 @@
import android.annotation.NonNull;
import android.car.Car;
import android.os.RemoteException;
+import android.util.Log;
/**
* Provides common Mockito calls for Car-specific classes.
*/
public final class CarMockitoHelper {
+ private static final String TAG = CarMockitoHelper.class.getSimpleName();
+
/**
* Mocks a call to {@link Car#handleRemoteExceptionFromCarService(RemoteException, Object)} so
* it returns the passed as 2nd argument.
@@ -35,7 +38,9 @@
public static void mockHandleRemoteExceptionFromCarServiceWithDefaultValue(
@NonNull Car car) {
doAnswer((invocation) -> {
- return invocation.getArguments()[1];
+ Object returnValue = invocation.getArguments()[1];
+ Log.v(TAG, "mocking handleRemoteExceptionFromCarService(): " + returnValue);
+ return returnValue;
}).when(car).handleRemoteExceptionFromCarService(isA(RemoteException.class), any());
}
diff --git a/service/src/com/android/car/CarShellCommand.java b/service/src/com/android/car/CarShellCommand.java
index 871e18c..fc6b2d2 100644
--- a/service/src/com/android/car/CarShellCommand.java
+++ b/service/src/com/android/car/CarShellCommand.java
@@ -26,6 +26,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.UiModeManager;
@@ -33,7 +34,7 @@
import android.car.input.CarInputManager;
import android.car.input.RotaryEvent;
import android.car.user.CarUserManager;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
import android.car.user.UserSwitchResult;
import android.car.userlib.HalCallback;
import android.car.userlib.UserHalHelper;
@@ -286,8 +287,11 @@
pw.println("\t Print this help text.");
pw.println("\tday-night-mode [day|night|sensor]");
pw.println("\t Force into day/night mode or restore to auto.");
- pw.println("\tinject-vhal-event property [zone] data(can be comma separated list)");
+ pw.println("\tinject-vhal-event property [zone] data(can be comma separated list) "
+ + "[-t delay_time_seconds]");
pw.println("\t Inject a vehicle property for testing.");
+ pw.println("\t delay_time_seconds: the event timestamp is increased by certain second.");
+ pw.println("\t If not specified, it will be 0.");
pw.println("\tinject-error-event property zone errorCode");
pw.println("\t Inject an error event from VHAL for testing.");
pw.println("\tenable-uxr true|false");
@@ -439,9 +443,12 @@
case COMMAND_INJECT_VHAL_EVENT:
String zone = PARAM_VEHICLE_PROPERTY_AREA_GLOBAL;
String data;
- if (args.length != 3 && args.length != 4) {
+ int argNum = args.length;
+ if (argNum < 3 || argNum > 6) {
return showInvalidArguments(writer);
- } else if (args.length == 4) {
+ }
+ String delayTime = args[argNum - 2].equals("-t") ? args[argNum - 1] : "0";
+ if (argNum == 4 || argNum == 6) {
// Zoned
zone = args[2];
data = args[3];
@@ -449,7 +456,7 @@
// Global
data = args[2];
}
- injectVhalEvent(args[1], zone, data, false, writer);
+ injectVhalEvent(args[1], zone, data, false, delayTime, writer);
break;
case COMMAND_INJECT_ERROR_EVENT:
if (args.length != 4) {
@@ -457,7 +464,7 @@
}
String errorAreaId = args[2];
String errorCode = args[3];
- injectVhalEvent(args[1], errorAreaId, errorCode, true, writer);
+ injectVhalEvent(args[1], errorAreaId, errorCode, true, "0", writer);
break;
case COMMAND_ENABLE_UXR:
if (args.length != 2) {
@@ -929,20 +936,10 @@
waitForHal(writer, latch, timeout);
return;
}
- Car car = Car.createCar(mContext);
- CarUserManager carUserManager =
- (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
+ CarUserManager carUserManager = getCarUserManager(mContext);
AndroidFuture<UserSwitchResult> future = carUserManager.switchUser(targetUserId);
- UserSwitchResult result = null;
- try {
- result = future.get(timeout, TimeUnit.MILLISECONDS);
- } catch (Exception e) {
- Log.e(TAG, "exception calling CarUserManager.switchUser(" + targetUserId + ")", e);
- }
- if (result == null) {
- writer.printf("Service didn't respond in %d ms", timeout);
- return;
- }
+ UserSwitchResult result = waitForFuture(writer, future, timeout);
+ if (result == null) return;
writer.printf("UserSwitchResult: status = %s\n",
UserSwitchResult.statusToString(result.getStatus()));
String msg = result.getErrorMessage();
@@ -951,6 +948,20 @@
}
}
+ private static <T> T waitForFuture(@NonNull PrintWriter writer,
+ @NonNull AndroidFuture<T> future, int timeoutMs) {
+ T result = null;
+ try {
+ result = future.get(timeoutMs, TimeUnit.MILLISECONDS);
+ if (result == null) {
+ writer.printf("Service didn't respond in %d ms", timeoutMs);
+ }
+ } catch (Exception e) {
+ writer.printf("Exception getting future: %s", e);
+ }
+ return result;
+ }
+
private void getInitialUser(PrintWriter writer) {
android.content.pm.UserInfo user = mCarUserService.getInitialUser();
writer.println(user == null ? NO_INITIAL_USER : user.id);
@@ -1004,24 +1015,21 @@
+ ", request=" + request);
UserIdentificationResponse response = mHal.getUserHal().getUserAssociation(request);
Log.d(TAG, "getUserAuthAssociation(): response=" + response);
-
- if (response == null) {
- writer.println("null response");
- return;
- }
-
- if (!TextUtils.isEmpty(response.errorMessage)) {
- writer.printf("Error message: %s\n", response.errorMessage);
- }
- int numberAssociations = response.associations.size();
- writer.printf("%d associations:\n", numberAssociations);
- for (int i = 0; i < numberAssociations; i++) {
- UserIdentificationAssociation association = response.associations.get(i);
- writer.printf(" %s\n", association);
- }
+ showResponse(writer, response);
return;
}
+ CarUserManager carUserManager = getCarUserManager(writer, userId);
+ int[] types = new int[requestSize];
+ for (int i = 0; i < requestSize; i++) {
+ types[i] = request.associationTypes.get(i);
+ }
+ UserIdentificationAssociationResponse response = carUserManager
+ .getUserIdentificationAssociation(types);
+ showResponse(writer, response);
+ }
+
+ private CarUserManager getCarUserManager(@NonNull PrintWriter writer, @UserIdInt int userId) {
Context context;
if (userId == mContext.getUserId()) {
context = mContext;
@@ -1034,14 +1042,35 @@
+ "what CarUserService will use when calling HAL.\n", userId, actualUserId);
}
+ return getCarUserManager(context);
+ }
+
+ private CarUserManager getCarUserManager(@NonNull Context context) {
Car car = Car.createCar(context);
CarUserManager carUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
- int[] types = new int[requestSize];
- for (int i = 0; i < types.length; i++) {
- types[i] = request.associationTypes.get(i);
+ return carUserManager;
+ }
+
+ private void showResponse(@NonNull PrintWriter writer,
+ @NonNull UserIdentificationResponse response) {
+ if (response == null) {
+ writer.println("null response");
+ return;
}
- GetUserIdentificationAssociationResponse response = carUserManager
- .getUserIdentificationAssociation(types);
+
+ if (!TextUtils.isEmpty(response.errorMessage)) {
+ writer.printf("Error message: %s\n", response.errorMessage);
+ }
+ int numberAssociations = response.associations.size();
+ writer.printf("%d associations:\n", numberAssociations);
+ for (int i = 0; i < numberAssociations; i++) {
+ UserIdentificationAssociation association = response.associations.get(i);
+ writer.printf(" %s\n", association);
+ }
+ }
+
+ private void showResponse(@NonNull PrintWriter writer,
+ @NonNull UserIdentificationAssociationResponse response) {
if (response == null) {
writer.println("null response");
return;
@@ -1058,7 +1087,7 @@
}
private void setUserAuthAssociation(String[] args, PrintWriter writer) {
- if (args.length < 4) {
+ if (args.length < 3) {
writer.println("invalid usage, must pass at least 4 arguments");
return;
}
@@ -1119,20 +1148,7 @@
mHal.getUserHal().setUserAssociation(timeout, request, (status, response) -> {
Log.d(TAG, "setUserAuthAssociation(): response=" + response);
try {
- if (response == null) {
- writer.println("null response");
- return;
- }
-
- if (!TextUtils.isEmpty(response.errorMessage)) {
- writer.printf("Error message: %s\n", response.errorMessage);
- }
- int numberAssociations = response.associations.size();
- writer.printf("%d associations:\n", numberAssociations);
- for (int i = 0; i < numberAssociations; i++) {
- UserIdentificationAssociation association = response.associations.get(i);
- writer.printf(" %s\n", association);
- }
+ showResponse(writer, response);
} finally {
latch.countDown();
}
@@ -1140,8 +1156,20 @@
waitForHal(writer, latch, timeout);
return;
}
- // TODO(b/150409351): implement it...
- throw new UnsupportedOperationException("must set --hal-only");
+ CarUserManager carUserManager = getCarUserManager(writer, userId);
+ int[] types = new int[requestSize];
+ int[] values = new int[requestSize];
+ for (int i = 0; i < requestSize; i++) {
+ UserIdentificationSetAssociation association = request.associations.get(i);
+ types[i] = association.type;
+ values[i] = association.value;
+ }
+ AndroidFuture<UserIdentificationAssociationResponse> future = carUserManager
+ .setUserIdentificationAssociation(types, values);
+ UserIdentificationAssociationResponse response = waitForFuture(writer, future, timeout);
+ if (response != null) {
+ showResponse(writer, response);
+ }
}
private static int parseAuthArg(@NonNull SparseArray<String> types, @NonNull String type) {
@@ -1218,10 +1246,11 @@
* @param zone Zone that this event services
* @param isErrorEvent indicates the type of event
* @param value Data value of the event
+ * @param delayTime the event timestamp is increased by delayTime
* @param writer PrintWriter
*/
private void injectVhalEvent(String property, String zone, String value,
- boolean isErrorEvent, PrintWriter writer) {
+ boolean isErrorEvent, String delayTime, PrintWriter writer) {
if (zone != null && (zone.equalsIgnoreCase(PARAM_VEHICLE_PROPERTY_AREA_GLOBAL))) {
if (!isPropertyAreaTypeGlobal(property)) {
writer.println("Property area type inconsistent with given zone");
@@ -1232,7 +1261,7 @@
if (isErrorEvent) {
mHal.injectOnPropertySetError(property, zone, value);
} else {
- mHal.injectVhalEvent(property, zone, value);
+ mHal.injectVhalEvent(property, zone, value, delayTime);
}
} catch (NumberFormatException e) {
writer.println("Invalid property Id zone Id or value" + e);
diff --git a/service/src/com/android/car/hal/UserHalService.java b/service/src/com/android/car/hal/UserHalService.java
index f995047..ce43be4 100644
--- a/service/src/com/android/car/hal/UserHalService.java
+++ b/service/src/com/android/car/hal/UserHalService.java
@@ -53,6 +53,8 @@
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import com.android.car.CarLocalServices;
+import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.car.EventLogTags;
@@ -95,7 +97,7 @@
// This handler handles 2 types of messages:
// - "Anonymous" messages (what=0) containing runnables.
// - "Identifiable" messages used to check for timeouts (whose 'what' is the request id).
- private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Handler mHandler;
/**
* Value used on the next request.
@@ -110,7 +112,13 @@
private final SparseArray<PendingRequest<?, ?>> mPendingRequests = new SparseArray<>();
public UserHalService(VehicleHal hal) {
+ this(hal, new Handler(Looper.getMainLooper()));
+ }
+
+ @VisibleForTesting
+ UserHalService(VehicleHal hal, Handler handler) {
mHal = hal;
+ mHandler = handler;
}
@Override
@@ -336,7 +344,7 @@
VehiclePropValue propRequest;
synchronized (mLock) {
checkSupportedLocked();
- int requestId = mNextRequestId++;
+ int requestId = getNextRequestId();
EventLog.writeEvent(EventLogTags.CAR_USER_HAL_LEGACY_SWITCH_USER_REQ, requestId,
targetInfo.userId, usersInfo.currentUser.userId);
propRequest = getPropRequestForSwitchUserLocked(requestId,
@@ -604,7 +612,7 @@
}
private void handleCheckIfRequestTimedOut(int requestId) {
- PendingRequest<?, ?> pendingRequest = getPendingResponse(requestId);
+ PendingRequest<?, ?> pendingRequest = getPendingRequest(requestId);
if (pendingRequest == null) return;
Log.w(TAG, "Request #" + requestId + " timed out");
@@ -613,7 +621,7 @@
}
@Nullable
- private PendingRequest<?, ?> getPendingResponse(int requestId) {
+ private PendingRequest<?, ?> getPendingRequest(int requestId) {
synchronized (mLock) {
return mPendingRequests.get(requestId);
}
@@ -666,6 +674,52 @@
private void handleOnSwitchUserResponse(VehiclePropValue value) {
int requestId = value.value.int32Values.get(0);
+ int messageType = value.value.int32Values.get(1);
+
+ if (messageType == SwitchUserMessageType.VEHICLE_RESPONSE) {
+ handleVehicleResponse(value);
+ return;
+ }
+
+ if (messageType == SwitchUserMessageType.VEHICLE_REQUEST) {
+ handleVehicleRequest(value);
+ return;
+ }
+
+ Log.e(TAG, "handleOnSwitchUserResponse invalid message type (" + messageType
+ + ") from HAL: " + value);
+
+ // check if a callback exists for the request ID
+ HalCallback<SwitchUserResponse> callback =
+ handleGetPendingCallback(requestId, SwitchUserResponse.class);
+ if (callback != null) {
+ handleRemovePendingRequest(requestId);
+ EventLog.writeEvent(EventLogTags.CAR_USER_HAL_SWITCH_USER_RESP, requestId,
+ HalCallback.STATUS_WRONG_HAL_RESPONSE, SwitchUserStatus.FAILURE);
+ callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, null);
+ return;
+ }
+ }
+
+ private void handleVehicleRequest(VehiclePropValue value) {
+ int requestId = value.value.int32Values.get(0);
+ // Index 1 is message type, which is not required in this call.
+ int targetUserId = value.value.int32Values.get(2);
+ EventLog.writeEvent(EventLogTags.CAR_USER_HAL_OEM_SWITCH_USER_REQ, requestId, targetUserId);
+
+ // HAL vehicle request should have negative request ID
+ if (requestId >= 0) {
+ Log.e(TAG, "handleVehicleRequest invalid requestId (" + requestId + ") from HAL: "
+ + value);
+ return;
+ }
+
+ CarUserService userService = CarLocalServices.getService(CarUserService.class);
+ userService.switchAndroidUserFromHal(requestId, targetUserId);
+ }
+
+ private void handleVehicleResponse(VehiclePropValue value) {
+ int requestId = value.value.int32Values.get(0);
HalCallback<SwitchUserResponse> callback =
handleGetPendingCallback(requestId, SwitchUserResponse.class);
if (callback == null) {
@@ -678,13 +732,6 @@
SwitchUserResponse response = new SwitchUserResponse();
response.requestId = requestId;
response.messageType = value.value.int32Values.get(1);
- if (response.messageType != SwitchUserMessageType.VEHICLE_RESPONSE) {
- EventLog.writeEvent(EventLogTags.CAR_USER_HAL_SWITCH_USER_RESP, requestId,
- HalCallback.STATUS_WRONG_HAL_RESPONSE);
- Log.e(TAG, "invalid message type (" + response.messageType + ") from HAL: " + value);
- callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, null);
- return;
- }
response.status = value.value.int32Values.get(2);
if (response.status == SwitchUserStatus.SUCCESS
|| response.status == SwitchUserStatus.FAILURE) {
@@ -703,7 +750,7 @@
}
private <T> HalCallback<T> handleGetPendingCallback(int requestId, Class<T> clazz) {
- PendingRequest<?, ?> pendingRequest = getPendingResponse(requestId);
+ PendingRequest<?, ?> pendingRequest = getPendingRequest(requestId);
if (pendingRequest == null) return null;
if (pendingRequest.responseClass != clazz) {
diff --git a/service/src/com/android/car/hal/VehicleHal.java b/service/src/com/android/car/hal/VehicleHal.java
index 7e80e74..8706c00 100644
--- a/service/src/com/android/car/hal/VehicleHal.java
+++ b/service/src/com/android/car/hal/VehicleHal.java
@@ -60,6 +60,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@@ -712,14 +713,16 @@
* @param property the Vehicle property Id as defined in the HAL
* @param zone Zone that this event services
* @param value Data value of the event
+ * @param delayTime Add a certain duration to event timestamp
*/
- public void injectVhalEvent(String property, String zone, String value)
+ public void injectVhalEvent(String property, String zone, String value, String delayTime)
throws NumberFormatException {
if (value == null || zone == null || property == null) {
return;
}
int propId = Integer.decode(property);
int zoneId = Integer.decode(zone);
+ int duration = Integer.decode(delayTime);
VehiclePropValue v = createPropValue(propId, zoneId);
int propertyType = propId & VehiclePropertyType.MASK;
// Values can be comma separated list
@@ -745,7 +748,7 @@
Log.e(CarLog.TAG_HAL, "Property type unsupported:" + propertyType);
return;
}
- v.timestamp = SystemClock.elapsedRealtimeNanos();
+ v.timestamp = SystemClock.elapsedRealtimeNanos() + TimeUnit.SECONDS.toNanos(duration);
onPropertyEvent(Lists.newArrayList(v));
}
diff --git a/service/src/com/android/car/user/CarUserService.java b/service/src/com/android/car/user/CarUserService.java
index 2ae56cb..00502d1 100644
--- a/service/src/com/android/car/user/CarUserService.java
+++ b/service/src/com/android/car/user/CarUserService.java
@@ -33,7 +33,7 @@
import android.car.user.CarUserManager.UserLifecycleEvent;
import android.car.user.CarUserManager.UserLifecycleEventType;
import android.car.user.CarUserManager.UserLifecycleListener;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
import android.car.user.UserSwitchResult;
import android.car.userlib.CarUserManagerHelper;
import android.car.userlib.CommonConstants.CarUserServiceConstants;
@@ -48,6 +48,8 @@
import android.hardware.automotive.vehicle.V2_0.SwitchUserStatus;
import android.hardware.automotive.vehicle.V2_0.UserIdentificationGetRequest;
import android.hardware.automotive.vehicle.V2_0.UserIdentificationResponse;
+import android.hardware.automotive.vehicle.V2_0.UserIdentificationSetAssociation;
+import android.hardware.automotive.vehicle.V2_0.UserIdentificationSetRequest;
import android.hardware.automotive.vehicle.V2_0.UsersInfo;
import android.location.LocationManager;
import android.os.Binder;
@@ -60,6 +62,7 @@
import android.os.UserManager;
import android.provider.Settings;
import android.sysprop.CarProperties;
+import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
import android.util.SparseArray;
@@ -867,7 +870,7 @@
}
@Override
- public GetUserIdentificationAssociationResponse getUserIdentificationAssociation(int[] types) {
+ public UserIdentificationAssociationResponse getUserIdentificationAssociation(int[] types) {
Preconditions.checkArgument(!ArrayUtils.isEmpty(types), "must have at least one type");
checkManageUsersPermission("getUserIdentificationAssociation");
@@ -888,7 +891,7 @@
if (halResponse == null) {
Log.w(TAG, "getUserIdentificationAssociation(): HAL returned null for "
+ Arrays.toString(types));
- return null;
+ return UserIdentificationAssociationResponse.forFailure();
}
int[] values = new int[halResponse.associations.size()];
@@ -897,7 +900,68 @@
}
EventLog.writeEvent(EventLogTags.CAR_USER_MGR_GET_USER_AUTH_RESP, values.length);
- return new GetUserIdentificationAssociationResponse(halResponse.errorMessage, values);
+ return UserIdentificationAssociationResponse.forSuccess(values, halResponse.errorMessage);
+ }
+
+ @Override
+ public void setUserIdentificationAssociation(int timeoutMs, int[] types, int[] values,
+ AndroidFuture<UserIdentificationAssociationResponse> result) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(types), "must have at least one type");
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(values), "must have at least one value");
+ if (types.length != values.length) {
+ throw new IllegalArgumentException("types (" + Arrays.toString(types) + ") and values ("
+ + Arrays.toString(values) + ") should have the same length");
+ }
+ checkManageUsersPermission("setUserIdentificationAssociation");
+
+ int uid = getCallingUid();
+ int userId = UserHandle.getUserId(uid);
+ EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_REQ, uid, userId, types.length);
+
+ UserIdentificationSetRequest request = new UserIdentificationSetRequest();
+ request.userInfo.userId = userId;
+ request.userInfo.flags = getHalUserInfoFlags(userId);
+
+ request.numberAssociations = types.length;
+ for (int i = 0; i < types.length; i++) {
+ UserIdentificationSetAssociation association = new UserIdentificationSetAssociation();
+ association.type = types[i];
+ association.value = values[i];
+ request.associations.add(association);
+ }
+
+ mHal.setUserAssociation(timeoutMs, request, (status, resp) -> {
+ if (status != HalCallback.STATUS_OK) {
+ Log.w(TAG, "setUserIdentificationAssociation(): invalid callback status ("
+ + UserHalHelper.halCallbackStatusToString(status) + ") for response "
+ + resp);
+ if (resp == null || TextUtils.isEmpty(resp.errorMessage)) {
+ EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_RESP, 0);
+ result.complete(UserIdentificationAssociationResponse.forFailure());
+ return;
+ }
+ EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_RESP, 0,
+ resp.errorMessage);
+ result.complete(
+ UserIdentificationAssociationResponse.forFailure(resp.errorMessage));
+ return;
+ }
+ int respSize = resp.associations.size();
+ EventLog.writeEvent(EventLogTags.CAR_USER_MGR_SET_USER_AUTH_RESP, respSize,
+ resp.errorMessage);
+
+ int[] responseTypes = new int[respSize];
+ for (int i = 0; i < respSize; i++) {
+ responseTypes[i] = resp.associations.get(i).value;
+ }
+ UserIdentificationAssociationResponse response = UserIdentificationAssociationResponse
+ .forSuccess(responseTypes, resp.errorMessage);
+ if (Log.isLoggable(TAG_USER, Log.DEBUG)) {
+ Log.d(TAG, "setUserIdentificationAssociation(): resp= " + resp
+ + ", converted=" + response);
+ }
+ result.complete(response);
+ });
}
/**
@@ -931,6 +995,49 @@
receiver.complete(new UserSwitchResult(status, errorMessage));
}
+ /**
+ * Calls activity manager for user switch.
+ *
+ * <p><b>NOTE</b> This method is meant to be called just by UserHalService.
+ *
+ * @param requestId for the user switch request
+ * @param targetUserId of the target user
+ *
+ * @hide
+ */
+ public void switchAndroidUserFromHal(int requestId, @UserIdInt int targetUserId) {
+ EventLog.writeEvent(EventLogTags.CAR_USER_SVC_SWITCH_USER_FROM_HAL_REQ, requestId,
+ targetUserId);
+ Log.i(TAG_USER, "User hal requested a user switch. Target user id " + targetUserId);
+
+ try {
+ boolean result = mAm.switchUser(targetUserId);
+ if (result) {
+ updateUserSwitchInProcess(requestId, targetUserId);
+ } else {
+ postSwitchHalResponse(requestId, targetUserId);
+ }
+ } catch (RemoteException e) {
+ // ignore
+ Log.w(TAG_USER, "error while switching user " + targetUserId, e);
+ }
+ }
+
+ private void updateUserSwitchInProcess(int requestId, @UserIdInt int targetUserId) {
+ synchronized (mLockUser) {
+ if (mUserIdForUserSwitchInProcess != UserHandle.USER_NULL) {
+ // Some other user switch is in process.
+ if (Log.isLoggable(TAG_USER, Log.DEBUG)) {
+ Log.d(TAG_USER, "User switch for user: " + mUserIdForUserSwitchInProcess
+ + " is in process. Abandoning it as a new user switch is requested"
+ + " for the target user: " + targetUserId);
+ }
+ }
+ mUserIdForUserSwitchInProcess = targetUserId;
+ mRequestIdForUserSwitchInProcess = requestId;
+ }
+ }
+
private void postSwitchHalResponse(int requestId, @UserIdInt int targetUserId) {
UserInfo targetUser = mUserManager.getUserInfo(targetUserId);
UsersInfo usersInfo = getUsersInfo();
@@ -963,7 +1070,7 @@
mUserSwitchUiReceiver = receiver;
}
- // TODO(b/144120654): use helper to generate UsersInfo
+ // TODO(b/150413515): use helper to generate UsersInfo
private UsersInfo getUsersInfo() {
UserInfo currentUser;
try {
@@ -975,7 +1082,7 @@
return getUsersInfo(currentUser);
}
- // TODO(b/144120654): use helper to generate UsersInfo
+ // TODO(b/150413515): use helper to generate UsersInfo
private UsersInfo getUsersInfo(@NonNull UserInfo currentUser) {
List<UserInfo> existingUsers = mUserManager.getUsers();
int size = existingUsers.size();
diff --git a/service/src/com/android/car/user/UserMetrics.java b/service/src/com/android/car/user/UserMetrics.java
index adba813..62b9755 100644
--- a/service/src/com/android/car/user/UserMetrics.java
+++ b/service/src/com/android/car/user/UserMetrics.java
@@ -65,7 +65,7 @@
// garage mode
private static final int INITIAL_CAPACITY = 2;
- // TODO(b/144120654): read from resources
+ // TODO(b/150413515): read from resources
private static final int LOG_SIZE = 10;
private final Object mLock = new Object();
diff --git a/tests/SecondaryHomeApp/Android.bp b/tests/SecondaryHomeApp/Android.bp
index ce8e763..04da06b 100644
--- a/tests/SecondaryHomeApp/Android.bp
+++ b/tests/SecondaryHomeApp/Android.bp
@@ -22,14 +22,10 @@
],
static_libs: [
- "android.car.userlib",
"androidx.appcompat_appcompat",
- "androidx.recyclerview_recyclerview",
- "androidx.legacy_legacy-support-v4",
"androidx.lifecycle_lifecycle-extensions",
"com.google.android.material_material",
"CarNotificationLib",
- "car-ui-lib"
],
libs: [
diff --git a/tests/SecondaryHomeApp/AndroidManifest.xml b/tests/SecondaryHomeApp/AndroidManifest.xml
index 0a84531..5d22b70 100644
--- a/tests/SecondaryHomeApp/AndroidManifest.xml
+++ b/tests/SecondaryHomeApp/AndroidManifest.xml
@@ -45,6 +45,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.SECONDARY_HOME"/>
</intent-filter>
</activity>
<service android:name=".launcher.NotificationListener"
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
index 033bb9a..67a9b6f 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
@@ -33,9 +33,11 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -60,12 +62,18 @@
import android.hardware.automotive.vehicle.V2_0.UsersInfo;
import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
+import android.os.Handler;
+import android.os.Looper;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
+import com.android.car.CarLocalServices;
+import com.android.car.user.CarUserService;
+
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -116,6 +124,10 @@
@Mock
private VehicleHal mVehicleHal;
+ @Mock
+ private CarUserService mCarUserService;
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
private final UserInfo mUser0 = new UserInfo();
private final UserInfo mUser10 = new UserInfo();
@@ -127,7 +139,7 @@
@Before
public void setFixtures() {
- mUserHalService = spy(new UserHalService(mVehicleHal));
+ mUserHalService = spy(new UserHalService(mVehicleHal, mHandler));
// Needs at least one property, otherwise isSupported() will return false
mUserHalService.takeProperties(Arrays.asList(newSubscribableConfig(INITIAL_USER_INFO)));
@@ -141,6 +153,13 @@
mUsersInfo.existingUsers = new ArrayList<>(2);
mUsersInfo.existingUsers.add(mUser0);
mUsersInfo.existingUsers.add(mUser10);
+
+ CarLocalServices.addService(CarUserService.class, mCarUserService);
+ }
+
+ @After
+ public void clearFixtures() {
+ CarLocalServices.removeServiceForTest(CarUserService.class);
}
@Test
@@ -464,7 +483,7 @@
@Test
public void testSwitchUser_halReturnedInvalidMessageType() throws Exception {
VehiclePropValue propResponse = UserHalHelper.createPropRequest(SWITCH_USER,
- REQUEST_ID_PLACE_HOLDER, SwitchUserMessageType.VEHICLE_REQUEST);
+ REQUEST_ID_PLACE_HOLDER, SwitchUserMessageType.LEGACY_ANDROID_SWITCH);
propResponse.value.int32Values.add(SwitchUserStatus.SUCCESS);
AtomicReference<VehiclePropValue> reqCaptor = replySetPropertyWithOnChangeEvent(
@@ -576,6 +595,35 @@
}
@Test
+ public void testUserSwitch_OEMRequest_success() throws Exception {
+ int requestId = -4;
+ int targetUserId = 11;
+ VehiclePropValue propResponse = UserHalHelper.createPropRequest(SWITCH_USER,
+ requestId, SwitchUserMessageType.VEHICLE_REQUEST);
+
+ propResponse.value.int32Values.add(targetUserId);
+
+ mUserHalService.onHalEvents(Arrays.asList(propResponse));
+ waitForHandler();
+
+ verify(mCarUserService).switchAndroidUserFromHal(requestId, targetUserId);
+ }
+
+ @Test
+ public void testUserSwitch_OEMRequest_failure_positiveRequestId() throws Exception {
+ int requestId = 4;
+ int targetUserId = 11;
+ VehiclePropValue propResponse = UserHalHelper.createPropRequest(SWITCH_USER,
+ requestId, SwitchUserMessageType.VEHICLE_REQUEST);
+ propResponse.value.int32Values.add(targetUserId);
+
+ mUserHalService.onHalEvents(Arrays.asList(propResponse));
+ waitForHandler();
+
+ verify(mCarUserService, never()).switchAndroidUserFromHal(anyInt(), anyInt());
+ }
+
+ @Test
public void testPostSwitchResponse_noUsersInfo() {
assertThrows(NullPointerException.class,
() -> mUserHalService.postSwitchResponse(42, mUser10, null));
@@ -1052,6 +1100,13 @@
return request;
}
+ /**
+ * Run empty runnable to make sure that all posted handlers are done.
+ */
+ private void waitForHandler() {
+ mHandler.runWithScissors(() -> { }, /* Default timeout */ CALLBACK_TIMEOUT_TIMEOUT);
+ }
+
private void mockNextRequestId(int requestId) {
doReturn(requestId).when(mUserHalService).getNextRequestId();
}
diff --git a/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java
index abfd48d..80318a7 100644
--- a/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/user/CarUserManagerUnitTest.java
@@ -15,9 +15,11 @@
*/
package com.android.car.user;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUsers;
import static android.car.test.util.UserTestingHelper.newUsers;
import static android.car.testapi.CarMockitoHelper.mockHandleRemoteExceptionFromCarServiceWithDefaultValue;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.UserHandle.USER_SYSTEM;
import static com.google.common.truth.Truth.assertThat;
@@ -26,8 +28,10 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
@@ -39,9 +43,11 @@
import android.car.ICarUserService;
import android.car.test.mocks.AbstractExtendedMockitoTestCase;
import android.car.user.CarUserManager;
+import android.car.user.CarUserManager.UserLifecycleListener;
import android.car.user.CarUserManager.UserSwitchUiCallback;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
import android.car.user.UserSwitchResult;
+import android.content.Context;
import android.content.pm.UserInfo;
import android.os.RemoteException;
import android.os.UserManager;
@@ -117,9 +123,68 @@
}
@Test
+ public void testAddListener_nullExecutor() {
+ mockInteractAcrossUsersPermission();
+
+ assertThrows(NullPointerException.class, () -> mMgr.addListener(null, (e) -> { }));
+ }
+
+ @Test
+ public void testAddListener_nullListener() {
+ mockInteractAcrossUsersPermission();
+
+ assertThrows(NullPointerException.class, () -> mMgr.addListener(Runnable::run, null));
+ }
+
+ @Test
+ public void testAddListener_sameListenerAddedTwice() {
+ mockInteractAcrossUsersPermission();
+
+ UserLifecycleListener listener = (e) -> { };
+
+ mMgr.addListener(Runnable::run, listener);
+ assertThrows(IllegalStateException.class, () -> mMgr.addListener(Runnable::run, listener));
+ }
+
+ @Test
+ public void testAddListener_differentListenersAddedTwice() {
+ mockInteractAcrossUsersPermission();
+
+ mMgr.addListener(Runnable::run, (e) -> { });
+ mMgr.addListener(Runnable::run, (e) -> { });
+ }
+
+ @Test
+ public void testRemoveListener_nullListener() {
+ mockInteractAcrossUsersPermission();
+
+ assertThrows(NullPointerException.class, () -> mMgr.removeListener(null));
+ }
+
+ @Test
+ public void testRemoveListener_notAddedBefore() {
+ mockInteractAcrossUsersPermission();
+
+ UserLifecycleListener listener = (e) -> { };
+
+ assertThrows(IllegalStateException.class, () -> mMgr.removeListener(listener));
+ }
+
+ @Test
+ public void testRemoveListener_addAndRemove() {
+ mockInteractAcrossUsersPermission();
+ UserLifecycleListener listener = (e) -> { };
+
+ mMgr.addListener(Runnable::run, listener);
+ mMgr.removeListener(listener);
+
+ // Make sure it was removed
+ assertThrows(IllegalStateException.class, () -> mMgr.removeListener(listener));
+ }
+
+ @Test
public void testSwitchUser_success() throws Exception {
- expectServiceSwitchUserSucceeds(11, UserSwitchResult.STATUS_SUCCESSFUL,
- "D'OH!");
+ expectServiceSwitchUserSucceeds(11, UserSwitchResult.STATUS_SUCCESSFUL, "D'OH!");
AndroidFuture<UserSwitchResult> future = mMgr.switchUser(11);
@@ -170,24 +235,108 @@
@Test
public void testGetUserIdentificationAssociation_remoteException() throws Exception {
+ int[] types = new int[] {1};
+ when(mService.getUserIdentificationAssociation(types))
+ .thenThrow(new RemoteException("D'OH!"));
mockHandleRemoteExceptionFromCarServiceWithDefaultValue(mCar);
- assertThrows(IllegalArgumentException.class,
- () -> mMgr.getUserIdentificationAssociation(new int[] {}));
+
+ assertThat(mMgr.getUserIdentificationAssociation(types)).isNull();
}
@Test
public void testGetUserIdentificationAssociation_ok() throws Exception {
int[] types = new int[] { 4, 8, 15, 16, 23, 42 };
- GetUserIdentificationAssociationResponse expectedResponse =
- new GetUserIdentificationAssociationResponse(null, new int[] {});
+ UserIdentificationAssociationResponse expectedResponse =
+ UserIdentificationAssociationResponse.forSuccess(types);
when(mService.getUserIdentificationAssociation(types)).thenReturn(expectedResponse);
- GetUserIdentificationAssociationResponse actualResponse =
+ UserIdentificationAssociationResponse actualResponse =
mMgr.getUserIdentificationAssociation(types);
assertThat(actualResponse).isSameAs(expectedResponse);
}
+ // TODO(b/155311595): remove once permission check is done only on service
+ private void mockInteractAcrossUsersPermission() {
+ Context context = mock(Context.class);
+ when(mCar.getContext()).thenReturn(context);
+ when(context.checkSelfPermission(INTERACT_ACROSS_USERS)).thenReturn(PERMISSION_GRANTED);
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_nullTypes() throws Exception {
+ assertThrows(IllegalArgumentException.class,
+ () -> mMgr.setUserIdentificationAssociation(null, new int[] {42}));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_emptyTypes() throws Exception {
+ assertThrows(IllegalArgumentException.class,
+ () -> mMgr.setUserIdentificationAssociation(new int[0], new int[] {42}));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_nullValues() throws Exception {
+ assertThrows(IllegalArgumentException.class,
+ () -> mMgr.setUserIdentificationAssociation(new int[] {42}, null));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_emptyValues() throws Exception {
+ assertThrows(IllegalArgumentException.class,
+ () -> mMgr.setUserIdentificationAssociation(new int[] {42}, new int[0]));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_sizeMismatch() throws Exception {
+ assertThrows(IllegalArgumentException.class,
+ () -> mMgr.setUserIdentificationAssociation(new int[] {1}, new int[] {2, 3}));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_remoteException() throws Exception {
+ int[] types = new int[] {1};
+ int[] values = new int[] {2};
+ doThrow(new RemoteException("D'OH!")).when(mService)
+ .setUserIdentificationAssociation(anyInt(), same(types), same(values), notNull());
+ mockHandleRemoteExceptionFromCarServiceWithDefaultValue(mCar);
+
+ AndroidFuture<UserIdentificationAssociationResponse> future =
+ mMgr.setUserIdentificationAssociation(types, values);
+
+ assertThat(future).isNotNull();
+ UserIdentificationAssociationResponse result = getResult(future);
+ assertThat(result.isSuccess()).isFalse();
+ assertThat(result.getValues()).isNull();
+ assertThat(result.getErrorMessage()).isNull();
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_ok() throws Exception {
+ int[] types = new int[] { 1, 2, 3 };
+ int[] values = new int[] { 10, 20, 30 };
+ doAnswer((inv) -> {
+ @SuppressWarnings("unchecked")
+ AndroidFuture<UserIdentificationAssociationResponse> future =
+ (AndroidFuture<UserIdentificationAssociationResponse>) inv.getArguments()[3];
+ UserIdentificationAssociationResponse response =
+ UserIdentificationAssociationResponse.forSuccess(values, "D'OH!");
+ future.complete(response);
+ return null;
+ }).when(mService)
+ .setUserIdentificationAssociation(anyInt(), same(types), same(values), notNull());
+ mockHandleRemoteExceptionFromCarServiceWithDefaultValue(mCar);
+
+ AndroidFuture<UserIdentificationAssociationResponse> future =
+ mMgr.setUserIdentificationAssociation(types, values);
+
+ assertThat(future).isNotNull();
+ UserIdentificationAssociationResponse result = getResult(future);
+ assertThat(result.isSuccess()).isTrue();
+ assertThat(result.getValues()).asList().containsAllOf(10, 20, 30).inOrder();
+ assertThat(result.getErrorMessage()).isEqualTo("D'OH!");
+ }
+
private void expectServiceSwitchUserSucceeds(@UserIdInt int userId,
@UserSwitchResult.Status int status, @Nullable String errorMessage)
throws RemoteException {
diff --git a/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java b/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
index f52e48d..f031686 100644
--- a/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/user/CarUserServiceTest.java
@@ -61,7 +61,7 @@
import android.car.user.CarUserManager.UserLifecycleEvent;
import android.car.user.CarUserManager.UserLifecycleEventType;
import android.car.user.CarUserManager.UserLifecycleListener;
-import android.car.user.GetUserIdentificationAssociationResponse;
+import android.car.user.UserIdentificationAssociationResponse;
import android.car.user.UserSwitchResult;
import android.car.userlib.CarUserManagerHelper;
import android.car.userlib.HalCallback;
@@ -79,6 +79,7 @@
import android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociation;
import android.hardware.automotive.vehicle.V2_0.UserIdentificationGetRequest;
import android.hardware.automotive.vehicle.V2_0.UserIdentificationResponse;
+import android.hardware.automotive.vehicle.V2_0.UserIdentificationSetRequest;
import android.hardware.automotive.vehicle.V2_0.UsersInfo;
import android.location.LocationManager;
import android.os.Bundle;
@@ -153,6 +154,8 @@
private final int mGetUserInfoRequestType = InitialUserInfoRequestType.COLD_BOOT;
private final AndroidFuture<UserSwitchResult> mUserSwitchFuture = new AndroidFuture<>();
+ private final AndroidFuture<UserIdentificationAssociationResponse> mUserAssociationRespFuture =
+ new AndroidFuture<>();
private final int mAsyncCallTimeoutMs = 100;
private final BlockingResultReceiver mReceiver =
new BlockingResultReceiver(mAsyncCallTimeoutMs);
@@ -953,6 +956,30 @@
}
@Test
+ public void testSwitchUser_OEMRequest_success() throws Exception {
+ mockExistingUsersAndCurrentUser(mAdminUser);
+ mockAmSwitchUser(mRegularUser, true);
+ int requestId = -1;
+
+ mCarUserService.switchAndroidUserFromHal(requestId, mRegularUser.id);
+ mockCurrentUser(mRegularUser);
+ sendUserUnlockedEvent(mRegularUser.id);
+
+ assertPostSwitch(requestId, mRegularUser.id, mRegularUser.id);
+ }
+
+ @Test
+ public void testSwitchUser_OEMRequest_failure() throws Exception {
+ mockExistingUsersAndCurrentUser(mAdminUser);
+ mockAmSwitchUser(mRegularUser, false);
+ int requestId = -1;
+
+ mCarUserService.switchAndroidUserFromHal(requestId, mRegularUser.id);
+
+ assertPostSwitch(requestId, mAdminUser.id, mRegularUser.id);
+ }
+
+ @Test
public void testGetUserInfo_nullReceiver() throws Exception {
assertThrows(NullPointerException.class, () -> mCarUserService
.getInitialUserInfo(mGetUserInfoRequestType, mAsyncCallTimeoutMs, null));
@@ -1099,39 +1126,133 @@
@Test
public void testGetUserIdentificationAssociation_service_returnNull() throws Exception {
- // Must use the real user id - and not mock it - as the service will infer the id from
- // the Binder call - it's not worth the effort of mocking that.
- int currentUserId = ActivityManager.getCurrentUser();
- Log.d(TAG, "testGetUserIdentificationAssociation_ok(): current user is " + currentUserId);
- UserInfo currentUser = mockUmGetUserInfo(mMockedUserManager, currentUserId,
- UserInfo.FLAG_ADMIN);
+ mockCurrentUserForBinderCalls();
// Not mocking service call, so it will return null
- GetUserIdentificationAssociationResponse response = mCarUserService
+ UserIdentificationAssociationResponse response = mCarUserService
.getUserIdentificationAssociation(new int[] { 108 });
- assertThat(response).isNull();
+ assertThat(response.isSuccess()).isFalse();
+ assertThat(response.getValues()).isNull();
+ assertThat(response.getErrorMessage()).isNull();
}
@Test
public void testGetUserIdentificationAssociation_ok() throws Exception {
- // Must use the real user id - and not mock it - as the service will infer the id from
- // the Binder call - it's not worth the effort of mocking that.
- int currentUserId = ActivityManager.getCurrentUser();
- Log.d(TAG, "testGetUserIdentificationAssociation_ok(): current user is " + currentUserId);
- UserInfo currentUser = mockUmGetUserInfo(mMockedUserManager, currentUserId,
- UserInfo.FLAG_ADMIN);
+ UserInfo currentUser = mockCurrentUserForBinderCalls();
int[] types = new int[] { 1, 2, 3 };
- mockHalGetUserIdentificationAssociation(currentUser, types, new int[] { 10, 20, 30 },
- "D'OH!");
+ int[] values = new int[] { 10, 20, 30 };
+ mockHalGetUserIdentificationAssociation(currentUser, types, values, "D'OH!");
- GetUserIdentificationAssociationResponse response = mCarUserService
+ UserIdentificationAssociationResponse response = mCarUserService
.getUserIdentificationAssociation(types);
- assertThat(response.getValues()).asList().containsExactly(10, 20, 30)
- .inOrder();
+ assertThat(response.isSuccess()).isTrue();
+ assertThat(response.getValues()).asList().containsAllOf(10, 20, 30).inOrder();
+ assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_nullTypes() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> mCarUserService
+ .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+ null, new int[] {42}, mUserAssociationRespFuture));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_emptyTypes() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> mCarUserService
+ .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+ new int[0], new int[] {42}, mUserAssociationRespFuture));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_nullValues() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> mCarUserService
+ .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+ new int[] {42}, null, mUserAssociationRespFuture));
+ }
+ @Test
+ public void testSetUserIdentificationAssociation_sizeMismatch() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> mCarUserService
+ .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+ new int[] {1}, new int[] {2, 2}, mUserAssociationRespFuture));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_nullFuture() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> mCarUserService
+ .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+ new int[] {42}, new int[] {42}, null));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_noPermission() throws Exception {
+ mockManageUsersPermission(android.Manifest.permission.MANAGE_USERS, false);
+ assertThrows(SecurityException.class, () -> mCarUserService
+ .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+ new int[] {42}, new int[] {42}, mUserAssociationRespFuture));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_noCurrentUser() throws Exception {
+ // Should fail because we're not mocking UserManager.getUserInfo() to set the flag
+ assertThrows(IllegalArgumentException.class, () -> mCarUserService
+ .setUserIdentificationAssociation(mAsyncCallTimeoutMs,
+ new int[] {42}, new int[] {42}, mUserAssociationRespFuture));
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_halFailedWithErrorMessage() throws Exception {
+ mockCurrentUserForBinderCalls();
+ mockHalSetUserIdentificationAssociationFailure("D'OH!");
+ int[] types = new int[] { 1, 2, 3 };
+ int[] values = new int[] { 10, 20, 30 };
+ mCarUserService.setUserIdentificationAssociation(mAsyncCallTimeoutMs, types, values,
+ mUserAssociationRespFuture);
+
+ UserIdentificationAssociationResponse response = getUserAssociationRespResult();
+
+ assertThat(response.isSuccess()).isFalse();
+ assertThat(response.getValues()).isNull();
+ assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
+
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_halFailedWithoutErrorMessage()
+ throws Exception {
+ mockCurrentUserForBinderCalls();
+ mockHalSetUserIdentificationAssociationFailure(/* errorMessage= */ null);
+ int[] types = new int[] { 1, 2, 3 };
+ int[] values = new int[] { 10, 20, 30 };
+ mCarUserService.setUserIdentificationAssociation(mAsyncCallTimeoutMs, types, values,
+ mUserAssociationRespFuture);
+
+ UserIdentificationAssociationResponse response = getUserAssociationRespResult();
+
+ assertThat(response.isSuccess()).isFalse();
+ assertThat(response.getValues()).isNull();
+ assertThat(response.getErrorMessage()).isNull();
+ }
+
+ @Test
+ public void testSetUserIdentificationAssociation_ok() throws Exception {
+ UserInfo currentUser = mockCurrentUserForBinderCalls();
+
+ int[] types = new int[] { 1, 2, 3 };
+ int[] values = new int[] { 10, 20, 30 };
+ mockHalSetUserIdentificationAssociationSuccess(currentUser, types, values, "D'OH!");
+
+ mCarUserService.setUserIdentificationAssociation(mAsyncCallTimeoutMs, types, values,
+ mUserAssociationRespFuture);
+
+ UserIdentificationAssociationResponse response = getUserAssociationRespResult();
+
+ assertThat(response.isSuccess()).isTrue();
+ assertThat(response.getValues()).asList().containsAllOf(10, 20, 30).inOrder();
assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
}
@@ -1161,6 +1282,12 @@
}
@NonNull
+ private UserIdentificationAssociationResponse getUserAssociationRespResult()
+ throws Exception {
+ return getResult(mUserAssociationRespFuture);
+ }
+
+ @NonNull
private <T> T getResult(@NonNull AndroidFuture<T> future) throws Exception {
try {
return future.get(mAsyncCallTimeoutMs, TimeUnit.MILLISECONDS);
@@ -1170,6 +1297,19 @@
}
/**
+ * This method must be called for cases where the service infers the user id of the caller
+ * using Binder - it's not worth the effort of mocking such (native) calls.
+ */
+ @NonNull
+ private UserInfo mockCurrentUserForBinderCalls() {
+ int currentUserId = ActivityManager.getCurrentUser();
+ Log.d(TAG, "testetUserIdentificationAssociation_ok(): current user is " + currentUserId);
+ UserInfo currentUser = mockUmGetUserInfo(mMockedUserManager, currentUserId,
+ UserInfo.FLAG_ADMIN);
+ return currentUser;
+ }
+
+ /**
* Mock calls that generate a {@code UsersInfo}.
*/
private void mockExistingUsersAndCurrentUser(@NonNull UserInfo user)
@@ -1272,6 +1412,53 @@
.thenReturn(response);
}
+ private void mockHalSetUserIdentificationAssociationSuccess(@NonNull UserInfo user,
+ @NonNull int[] types, @NonNull int[] values, @Nullable String errorMessage) {
+ assertWithMessage("mismatch on number of types and values").that(types.length)
+ .isEqualTo(values.length);
+
+ UserIdentificationResponse response = new UserIdentificationResponse();
+ response.numberAssociation = types.length;
+ response.errorMessage = errorMessage;
+ for (int i = 0; i < types.length; i++) {
+ UserIdentificationAssociation association = new UserIdentificationAssociation();
+ association.type = types[i];
+ association.value = values[i];
+ response.associations.add(association);
+ }
+
+ doAnswer((invocation) -> {
+ Log.d(TAG, "Answering " + invocation + " with " + response);
+ @SuppressWarnings("unchecked")
+ UserIdentificationSetRequest request =
+ (UserIdentificationSetRequest) invocation.getArguments()[1];
+ assertWithMessage("Wrong user on %s", request)
+ .that(request.userInfo.userId)
+ .isEqualTo(user.id);
+ assertWithMessage("Wrong flags on %s", request)
+ .that(UserHalHelper.toUserInfoFlags(request.userInfo.flags))
+ .isEqualTo(user.flags);
+ @SuppressWarnings("unchecked")
+ HalCallback<UserIdentificationResponse> callback =
+ (HalCallback<UserIdentificationResponse>) invocation.getArguments()[2];
+ callback.onResponse(HalCallback.STATUS_OK, response);
+ return null;
+ }).when(mUserHal).setUserAssociation(eq(mAsyncCallTimeoutMs), notNull(), notNull());
+ }
+
+ private void mockHalSetUserIdentificationAssociationFailure(@NonNull String errorMessage) {
+ UserIdentificationResponse response = new UserIdentificationResponse();
+ response.errorMessage = errorMessage;
+ doAnswer((invocation) -> {
+ Log.d(TAG, "Answering " + invocation + " with " + response);
+ @SuppressWarnings("unchecked")
+ HalCallback<UserIdentificationResponse> callback =
+ (HalCallback<UserIdentificationResponse>) invocation.getArguments()[2];
+ callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, response);
+ return null;
+ }).when(mUserHal).setUserAssociation(eq(mAsyncCallTimeoutMs), notNull(), notNull());
+ }
+
private void mockManageUsersPermission(String permission, boolean granted) {
int result;
if (granted) {
diff --git a/tests/carservice_unit_test/src/com/android/car/user/UserIdentificationAssociationResponseTest.java b/tests/carservice_unit_test/src/com/android/car/user/UserIdentificationAssociationResponseTest.java
new file mode 100644
index 0000000..f9ca16d
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/user/UserIdentificationAssociationResponseTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.user;
+
+import static android.car.user.UserIdentificationAssociationResponse.forFailure;
+import static android.car.user.UserIdentificationAssociationResponse.forSuccess;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.car.user.UserIdentificationAssociationResponse;
+
+import org.junit.Test;
+
+public final class UserIdentificationAssociationResponseTest {
+
+ @Test
+ public void testFailure_noMessage() {
+ UserIdentificationAssociationResponse response = forFailure();
+
+ assertThat(response).isNotNull();
+ assertThat(response.isSuccess()).isFalse();
+ assertThat(response.getErrorMessage()).isNull();
+ assertThat(response.getValues()).isNull();
+ }
+
+ @Test
+ public void testFailure_withMessage() {
+ UserIdentificationAssociationResponse response = forFailure("D'OH!");
+
+ assertThat(response).isNotNull();
+ assertThat(response.isSuccess()).isFalse();
+ assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
+ assertThat(response.getValues()).isNull();
+ }
+
+ @Test
+ public void testSuccess_nullValues() {
+ assertThrows(IllegalArgumentException.class, () -> forSuccess(null));
+ }
+
+ @Test
+ public void testSuccess_nullValuesWithMessage() {
+ assertThrows(IllegalArgumentException.class, () -> forSuccess(null, "D'OH!"));
+ }
+
+ @Test
+ public void testSuccess_emptyValues() {
+ assertThrows(IllegalArgumentException.class, () -> forSuccess(new int[0]));
+ }
+
+ @Test
+ public void testSuccess_emptyValuesWithMessage() {
+ assertThrows(IllegalArgumentException.class, () -> forSuccess(new int[0], "D'OH!"));
+ }
+
+ @Test
+ public void testSuccess_noMessage() {
+ UserIdentificationAssociationResponse response = forSuccess(new int[] {1, 2, 3});
+
+ assertThat(response).isNotNull();
+ assertThat(response.isSuccess()).isTrue();
+ assertThat(response.getErrorMessage()).isNull();
+ assertThat(response.getValues()).asList().containsAllOf(1, 2, 3).inOrder();
+ }
+
+ @Test
+ public void testSuccess_withMessage() {
+ UserIdentificationAssociationResponse response = forSuccess(new int[] {1, 2, 3}, "D'OH!");
+
+ assertThat(response).isNotNull();
+ assertThat(response.isSuccess()).isTrue();
+ assertThat(response.getErrorMessage()).isEqualTo("D'OH!");
+ assertThat(response.getValues()).asList().containsAllOf(1, 2, 3).inOrder();
+ }
+}