Merge "Handle power cycle change in car watchdog daemon" into rvc-dev
diff --git a/service/src/com/android/car/hal/UserHalService.java b/service/src/com/android/car/hal/UserHalService.java
index 8d7fac6..595ada5 100644
--- a/service/src/com/android/car/hal/UserHalService.java
+++ b/service/src/com/android/car/hal/UserHalService.java
@@ -134,6 +134,10 @@
                     mHandler.sendMessage(obtainMessage(
                             UserHalService::handleOnInitialUserInfoResponse, this, value));
                     break;
+                case SWITCH_USER:
+                    mHandler.sendMessage(obtainMessage(
+                            UserHalService::handleOnSwicthUserResponse, this, value));
+                    break;
                 default:
                     Slog.w(TAG, "received unsupported event from HAL: " + value);
             }
@@ -188,7 +192,7 @@
      * {@link android.hardware.automotive.vehicle.V2_0.InitialUserInfoRequestType}).
      * @param timeoutMs how long to wait (in ms) for the property change event.
      * @param usersInfo current state of Android users.
-     * @param callback callback to handle the response.
+     * @param callback to handle the response.
      *
      * @throws IllegalStateException if the HAL does not support user management (callers should
      * call {@link #isSupported()} first to avoid this exception).
@@ -209,17 +213,9 @@
             checkSupportedLocked();
             if (hasPendingRequestLocked(InitialUserInfoResponse.class, callback)) return;
             requestId = mNextRequestId++;
-            // TODO(b/150413515): use helper method to convert request to prop value
             propRequest.value.int32Values.add(requestId);
             propRequest.value.int32Values.add(requestType);
-            propRequest.value.int32Values.add(usersInfo.currentUser.userId);
-            propRequest.value.int32Values.add(usersInfo.currentUser.flags);
-            propRequest.value.int32Values.add(usersInfo.numberUsers);
-            for (int i = 0; i < usersInfo.numberUsers; i++) {
-                UserInfo userInfo = usersInfo.existingUsers.get(i);
-                propRequest.value.int32Values.add(userInfo.userId);
-                propRequest.value.int32Values.add(userInfo.flags);
-            }
+            addUsersInfo(propRequest, usersInfo);
             setTimestamp(propRequest);
             addPendingRequestLocked(requestId, InitialUserInfoResponse.class, callback);
         }
@@ -238,18 +234,67 @@
     }
 
     /**
-     * TODO(b/150409110): javadoc it :-)
+     * Calls HAL to asynchronously switch user.
+     *
+     * @param targetInfo target user for user switching
+     * @param timeoutMs how long to wait (in ms) for the property change event.
+     * @param usersInfo current state of Android users.
+     * @param callback to handle the response.
+     *
+     * @throws IllegalStateException if the HAL does not support user management (callers should
+     * call {@link #isSupported()} first to avoid this exception).
      */
     public void switchUser(@NonNull UserInfo targetInfo, int timeoutMs,
             @NonNull UsersInfo usersInfo, @NonNull HalCallback<SwitchUserResponse> callback) {
         if (DBG) Log.d(TAG, "switchUser(" + targetInfo + ")");
+        Preconditions.checkArgumentPositive(timeoutMs, "timeout must be positive");
+        Objects.requireNonNull(usersInfo);
+        // TODO(b/150413515): use helper method to convert request to prop value and check usersInfo
+        // is valid
+        Objects.requireNonNull(callback);
 
-        // TODO(b/150409110): implement
-        SwitchUserResponse response = new SwitchUserResponse();
-        response.messageType = SwitchUserMessageType.VEHICLE_RESPONSE;
-        response.status = SwitchUserStatus.SUCCESS;
-        response.requestId = mNextRequestId++;
-        callback.onResponse(HalCallback.STATUS_OK, response);
+        VehiclePropValue propRequest = new VehiclePropValue();
+        propRequest.prop = SWITCH_USER;
+        int requestId;
+        synchronized (mLock) {
+            checkSupportedLocked();
+            if (hasPendingRequestLocked(SwitchUserResponse.class, callback)) return;
+            requestId = mNextRequestId++;
+            // TODO(b/150413515): use helper method to convert request to prop value
+            propRequest.value.int32Values.add(requestId);
+            propRequest.value.int32Values.add(SwitchUserMessageType.ANDROID_SWITCH);
+            propRequest.value.int32Values.add(targetInfo.userId);
+            propRequest.value.int32Values.add(targetInfo.flags);
+            addUsersInfo(propRequest, usersInfo);
+            setTimestamp(propRequest);
+            addPendingRequestLocked(requestId, SwitchUserResponse.class, callback);
+        }
+
+        mHandler.sendMessageDelayed(
+                obtainMessage(UserHalService::handleCheckIfRequestTimedOut, this, requestId)
+                        .setWhat(requestId),
+                timeoutMs);
+
+        try {
+            if (DBG) Log.d(TAG, "Calling hal.set(): " + propRequest);
+            mHal.set(propRequest);
+        } catch (ServiceSpecificException e) {
+            handleRemovePendingRequest(requestId);
+            Log.w(TAG, "Failed to set ANDROID SWITCH", e);
+            callback.onResponse(HalCallback.STATUS_HAL_SET_TIMEOUT, null);
+        }
+    }
+
+    private static void addUsersInfo(VehiclePropValue propRequest, @NonNull UsersInfo usersInfo) {
+        // TODO(b/150419600) it should be moved to UserHalHelper and tested
+        propRequest.value.int32Values.add(usersInfo.currentUser.userId);
+        propRequest.value.int32Values.add(usersInfo.currentUser.flags);
+        propRequest.value.int32Values.add(usersInfo.numberUsers);
+        for (int i = 0; i < usersInfo.numberUsers; i++) {
+            UserInfo userInfo = usersInfo.existingUsers.get(i);
+            propRequest.value.int32Values.add(userInfo.userId);
+            propRequest.value.int32Values.add(userInfo.flags);
+        }
     }
 
     @GuardedBy("mLock")
@@ -345,6 +390,36 @@
         callback.onResponse(HalCallback.STATUS_OK, response);
     }
 
+    private void handleOnSwicthUserResponse(VehiclePropValue value) {
+        int requestId = value.value.int32Values.get(0);
+        HalCallback<SwitchUserResponse> callback =
+                handleGetPendingCallback(requestId, SwitchUserResponse.class);
+        if (callback == null) {
+            Log.w(TAG, "no callback for requestId " + requestId + ": " + value);
+            return;
+        }
+        handleRemovePendingRequest(requestId);
+        SwitchUserResponse response = new SwitchUserResponse();
+        response.requestId = requestId;
+        response.messageType = value.value.int32Values.get(1);
+        if (response.messageType != SwitchUserMessageType.VEHICLE_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) {
+            if (DBG) {
+                Log.d(TAG, "replying to request " + requestId + " with " + response);
+            }
+            callback.onResponse(HalCallback.STATUS_OK, response);
+        } else {
+            Log.e(TAG, "invalid status (" + response.status + ") from HAL: " + value);
+            callback.onResponse(HalCallback.STATUS_WRONG_HAL_RESPONSE, null);
+        }
+    }
+
     private <T> HalCallback<T> handleGetPendingCallback(int requestId, Class<T> clazz) {
         Pair<Class<?>, HalCallback<?>> pair = getPendingCallback(requestId);
         if (pair == null) return null;
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/system_feature_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/system_feature_fragment.xml
new file mode 100644
index 0000000..45d1dbb
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/system_feature_fragment.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<ListView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/sys_features_list"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+</ListView>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index 6e36cf3..4b5b568 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -64,6 +64,7 @@
 import com.google.android.car.kitchensink.sensor.SensorsTestFragment;
 import com.google.android.car.kitchensink.storagelifetime.StorageLifetimeFragment;
 import com.google.android.car.kitchensink.storagevolumes.StorageVolumesFragment;
+import com.google.android.car.kitchensink.systemfeatures.SystemFeaturesFragment;
 import com.google.android.car.kitchensink.touch.TouchTestFragment;
 import com.google.android.car.kitchensink.users.UsersFragment;
 import com.google.android.car.kitchensink.vehiclectrl.VehicleCtrlFragment;
@@ -187,6 +188,7 @@
             new FragmentMenuEntry("sensors", SensorsTestFragment.class),
             new FragmentMenuEntry("storage lifetime", StorageLifetimeFragment.class),
             new FragmentMenuEntry("storage volumes", StorageVolumesFragment.class),
+            new FragmentMenuEntry("system features", SystemFeaturesFragment.class),
             new FragmentMenuEntry("touch test", TouchTestFragment.class),
             new FragmentMenuEntry("users", UsersFragment.class),
             new FragmentMenuEntry("vehicle ctrl", VehicleCtrlFragment.class),
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/systemfeatures/SystemFeaturesFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/systemfeatures/SystemFeaturesFragment.java
new file mode 100644
index 0000000..f3d51b2
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/systemfeatures/SystemFeaturesFragment.java
@@ -0,0 +1,92 @@
+/*
+ * 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.google.android.car.kitchensink.systemfeatures;
+
+import android.annotation.Nullable;
+import android.content.pm.FeatureInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.car.kitchensink.R;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * Shows system features as available by PackageManager
+ */
+public class SystemFeaturesFragment extends Fragment {
+    private static final String TAG = "CAR.SYSFEATURES.KS";
+
+    private ListView mSysFeaturesList;
+    private PackageManager mPackageManager;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        mPackageManager = Objects.requireNonNull(
+                getContext().getPackageManager());
+
+        super.onCreate(savedInstanceState);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        mSysFeaturesList = (ListView) inflater.inflate(R.layout.system_feature_fragment,
+                container, false);
+        return mSysFeaturesList;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        refresh();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+    }
+
+    private void refresh() {
+        final FeatureInfo[] features = mPackageManager.getSystemAvailableFeatures();
+        if (features != null) {
+            final String[] descriptions = Arrays.stream(features)
+                .filter(fi -> fi != null && fi.name != null)
+                .sorted(Comparator.<FeatureInfo, String>comparing(fi -> fi.name)
+                                  .thenComparing(fi -> fi.version))
+                .map(fi -> String.format("%s (v=%d)", fi.name, fi.version))
+                .toArray(String[]::new);
+            mSysFeaturesList.setAdapter(new ArrayAdapter<>(getContext(),
+                    android.R.layout.simple_list_item_1, descriptions));
+        } else {
+            Log.e(TAG, "no features available on this device!");
+        }
+    }
+}
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 159efa1..622c66b 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
@@ -35,6 +35,9 @@
 import android.car.userlib.UserHalHelper;
 import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponse;
 import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
+import android.hardware.automotive.vehicle.V2_0.SwitchUserMessageType;
+import android.hardware.automotive.vehicle.V2_0.SwitchUserResponse;
+import android.hardware.automotive.vehicle.V2_0.SwitchUserStatus;
 import android.hardware.automotive.vehicle.V2_0.UserFlags;
 import android.hardware.automotive.vehicle.V2_0.UserInfo;
 import android.hardware.automotive.vehicle.V2_0.UsersInfo;
@@ -69,22 +72,21 @@
     private static final String TAG = UserHalServiceTest.class.getSimpleName();
 
     /**
-     * Timeout passed to {@link UserHalService#getInitialUserInfo(int, int, UsersInfo, HalCallback)}
-     * calls.
+     * Timeout passed to {@link UserHalService} methods
      */
-    private static final int INITIAL_USER_TIMEOUT_MS = 20;
+    private static final int TIMEOUT_MS = 20;
 
     /**
      * Timeout for {@link GenericHalCallback#assertCalled()} for tests where the HAL is supposed to
      * return something - it's a short time so it doesn't impact the test duration.
      */
-    private static final int INITIAL_USER_CALLBACK_TIMEOUT_SUCCESS = INITIAL_USER_TIMEOUT_MS + 50;
+    private static final int CALLBACK_TIMEOUT_SUCCESS = TIMEOUT_MS + 50;
 
     /**
      * Timeout for {@link GenericHalCallback#assertCalled()} for tests where the HAL is not supposed
      * to return anything - it's a slightly longer to make sure the test doesn't fail prematurely.
      */
-    private static final int INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT = INITIAL_USER_TIMEOUT_MS + 500;
+    private static final int CALLBACK_TIMEOUT_TIMEOUT = TIMEOUT_MS + 500;
 
     // Used when crafting a reqquest property - the real value will be set by the mock.
     private static final int REQUEST_ID_PLACE_HOLDER = 42;
@@ -167,7 +169,7 @@
     @Test
     public void testGetUserInfo_noUsersInfo() {
         assertThrows(NullPointerException.class,
-                () -> mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, null,
+                () -> mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, null,
                         (i, r) -> {
                         }));
     }
@@ -175,7 +177,7 @@
     @Test
     public void testGetUserInfo_noCallback() {
         assertThrows(NullPointerException.class,
-                () -> mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS,
+                () -> mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS,
                         mUsersInfo, null));
     }
 
@@ -184,8 +186,8 @@
         replySetPropertyWithTimeoutException(INITIAL_USER_INFO);
 
         GenericHalCallback<InitialUserInfoResponse> callback = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback);
 
         callback.assertCalled();
@@ -193,15 +195,15 @@
         assertThat(callback.response).isNull();
 
         // Make sure the pending request was removed
-        SystemClock.sleep(INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT);
+        SystemClock.sleep(CALLBACK_TIMEOUT_TIMEOUT);
         callback.assertNotCalledAgain();
     }
 
     @Test
     public void testGetUserInfo_halDidNotReply() throws Exception {
         GenericHalCallback<InitialUserInfoResponse> callback = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback);
 
         callback.assertCalled();
@@ -212,12 +214,12 @@
     @Test
     public void testGetUserInfo_secondCallFailWhilePending() throws Exception {
         GenericHalCallback<InitialUserInfoResponse> callback1 = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT);
+                CALLBACK_TIMEOUT_TIMEOUT);
         GenericHalCallback<InitialUserInfoResponse> callback2 = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback1);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback2);
 
         callback1.assertCalled();
@@ -240,8 +242,8 @@
                 /* rightRequestId= */ false);
 
         GenericHalCallback<InitialUserInfoResponse> callback = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback);
 
         callback.assertCalled();
@@ -261,8 +263,8 @@
                 INITIAL_USER_INFO, propResponse, /* rightRequestId= */ true);
 
         GenericHalCallback<InitialUserInfoResponse> callback = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_SUCCESS);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                CALLBACK_TIMEOUT_SUCCESS);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback);
 
         callback.assertCalled();
@@ -287,8 +289,8 @@
                 INITIAL_USER_INFO, propResponse, /* rightRequestId= */ true);
 
         GenericHalCallback<InitialUserInfoResponse> callback = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_SUCCESS);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                CALLBACK_TIMEOUT_SUCCESS);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback);
 
         callback.assertCalled();
@@ -320,8 +322,8 @@
                 INITIAL_USER_INFO, propResponse, /* rightRequestId= */ true);
 
         GenericHalCallback<InitialUserInfoResponse> callback = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_SUCCESS);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                CALLBACK_TIMEOUT_SUCCESS);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback);
 
         callback.assertCalled();
@@ -355,8 +357,8 @@
                 INITIAL_USER_INFO, propResponse, /* rightRequestId= */ true);
 
         GenericHalCallback<InitialUserInfoResponse> callback = new GenericHalCallback<>(
-                INITIAL_USER_CALLBACK_TIMEOUT_SUCCESS);
-        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                CALLBACK_TIMEOUT_SUCCESS);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, TIMEOUT_MS, mUsersInfo,
                 callback);
 
         callback.assertCalled();
@@ -381,6 +383,204 @@
         testGetUserInfo_successDefault();
     }
 
+    @Test
+    public void testSwitchUser_invalidTimeout() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mUserHalService.switchUser(mUser10, 0, mUsersInfo, (i, r) -> {
+                }));
+        assertThrows(IllegalArgumentException.class,
+                () -> mUserHalService.switchUser(mUser10, -1, mUsersInfo, (i, r) -> {
+                }));
+    }
+
+    @Test
+    public void testSwitchUser_noUsersInfo() {
+        assertThrows(NullPointerException.class,
+                () -> mUserHalService.switchUser(mUser10, TIMEOUT_MS, null,
+                        (i, r) -> {
+                        }));
+    }
+
+    @Test
+    public void testSwitchUser_noCallback() {
+        assertThrows(NullPointerException.class,
+                () -> mUserHalService.switchUser(mUser10, TIMEOUT_MS,
+                        mUsersInfo, null));
+    }
+
+    @Test
+    public void testSwitchUser_halSetTimedOut() throws Exception {
+        replySetPropertyWithTimeoutException(SWITCH_USER);
+
+        GenericHalCallback<SwitchUserResponse> callback = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback);
+
+        callback.assertCalled();
+        assertCallbackStatus(callback, HalCallback.STATUS_HAL_SET_TIMEOUT);
+        assertThat(callback.response).isNull();
+
+        // Make sure the pending request was removed
+        SystemClock.sleep(CALLBACK_TIMEOUT_TIMEOUT);
+        callback.assertNotCalledAgain();
+    }
+
+    @Test
+    public void testSwitchUser_halDidNotReply() throws Exception {
+        GenericHalCallback<SwitchUserResponse> callback = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback);
+
+        callback.assertCalled();
+        assertCallbackStatus(callback, HalCallback.STATUS_HAL_RESPONSE_TIMEOUT);
+        assertThat(callback.response).isNull();
+    }
+
+    @Test
+    public void testSwitchUser_halReplyWithWrongRequestId() throws Exception {
+        // TODO(b/150419600): use helper method to convert prop value to proper req
+        VehiclePropValue propResponse = new VehiclePropValue();
+        propResponse.prop = SWITCH_USER;
+        propResponse.value.int32Values.add(REQUEST_ID_PLACE_HOLDER);
+
+        replySetPropertyWithOnChangeEvent(SWITCH_USER, propResponse,
+                /* rightRequestId= */ false);
+
+        GenericHalCallback<SwitchUserResponse> callback = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback);
+
+        callback.assertCalled();
+        assertCallbackStatus(callback, HalCallback.STATUS_HAL_RESPONSE_TIMEOUT);
+        assertThat(callback.response).isNull();
+    }
+
+    @Test
+    public void testSwitchUser_halReturnedInvalidMessageType() throws Exception {
+        // TODO(b/150419600): use helper method to convert prop value to proper req
+        VehiclePropValue propResponse = new VehiclePropValue();
+        propResponse.prop = SWITCH_USER;
+        propResponse.value.int32Values.add(REQUEST_ID_PLACE_HOLDER);
+        propResponse.value.int32Values.add(SwitchUserMessageType.VEHICLE_REQUEST);
+        propResponse.value.int32Values.add(SwitchUserStatus.SUCCESS);
+
+        AtomicReference<VehiclePropValue> reqCaptor = replySetPropertyWithOnChangeEvent(
+                SWITCH_USER, propResponse, /* rightRequestId= */ true);
+
+        GenericHalCallback<SwitchUserResponse> callback = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_SUCCESS);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback);
+
+        callback.assertCalled();
+
+        // Make sure the arguments were properly converted
+        assertSwitchUserSetRequest(reqCaptor.get(), SwitchUserMessageType.ANDROID_SWITCH, mUser10);
+
+        // Assert response
+        assertCallbackStatus(callback, HalCallback.STATUS_WRONG_HAL_RESPONSE);
+        assertThat(callback.response).isNull();
+    }
+
+    @Test
+    public void testUserSwitch_success() throws Exception {
+        // TODO(b/150419600): use helper method to convert prop value to proper req
+        VehiclePropValue propResponse = new VehiclePropValue();
+        propResponse.prop = SWITCH_USER;
+        propResponse.value.int32Values.add(REQUEST_ID_PLACE_HOLDER);
+        propResponse.value.int32Values.add(SwitchUserMessageType.VEHICLE_RESPONSE);
+        propResponse.value.int32Values.add(SwitchUserStatus.SUCCESS);
+
+        AtomicReference<VehiclePropValue> reqCaptor = replySetPropertyWithOnChangeEvent(
+                SWITCH_USER, propResponse, /* rightRequestId= */ true);
+
+        GenericHalCallback<SwitchUserResponse> callback = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_SUCCESS);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback);
+
+        callback.assertCalled();
+
+        // Make sure the arguments were properly converted
+        assertSwitchUserSetRequest(reqCaptor.get(), SwitchUserMessageType.ANDROID_SWITCH, mUser10);
+
+        // Assert response
+        assertCallbackStatus(callback, HalCallback.STATUS_OK);
+        SwitchUserResponse actualResponse = callback.response;
+        assertThat(actualResponse.status).isEqualTo(SwitchUserStatus.SUCCESS);
+        assertThat(actualResponse.messageType).isEqualTo(SwitchUserMessageType.VEHICLE_RESPONSE);
+    }
+
+    @Test
+    public void testUserSwitch_failure() throws Exception {
+        // TODO(b/150419600): use helper method to convert prop value to proper req
+        VehiclePropValue propResponse = new VehiclePropValue();
+        propResponse.prop = SWITCH_USER;
+        propResponse.value.int32Values.add(REQUEST_ID_PLACE_HOLDER);
+        propResponse.value.int32Values.add(SwitchUserMessageType.VEHICLE_RESPONSE);
+        propResponse.value.int32Values.add(SwitchUserStatus.FAILURE);
+
+        AtomicReference<VehiclePropValue> reqCaptor = replySetPropertyWithOnChangeEvent(
+                SWITCH_USER, propResponse, /* rightRequestId= */ true);
+
+        GenericHalCallback<SwitchUserResponse> callback = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_SUCCESS);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback);
+
+        callback.assertCalled();
+
+        // Make sure the arguments were properly converted
+        assertSwitchUserSetRequest(reqCaptor.get(), SwitchUserMessageType.ANDROID_SWITCH, mUser10);
+
+        // Assert response
+        assertCallbackStatus(callback, HalCallback.STATUS_OK);
+        SwitchUserResponse actualResponse = callback.response;
+        assertThat(actualResponse.status).isEqualTo(SwitchUserStatus.FAILURE);
+        assertThat(actualResponse.messageType).isEqualTo(SwitchUserMessageType.VEHICLE_RESPONSE);
+    }
+
+    @Test
+    public void testSwitchUser_secondCallFailWhilePending() throws Exception {
+        GenericHalCallback<SwitchUserResponse> callback1 = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_TIMEOUT);
+        GenericHalCallback<SwitchUserResponse> callback2 = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback1);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback2);
+
+        callback1.assertCalled();
+        assertCallbackStatus(callback1, HalCallback.STATUS_HAL_RESPONSE_TIMEOUT);
+        assertThat(callback1.response).isNull();
+
+        callback2.assertCalled();
+        assertCallbackStatus(callback2, HalCallback.STATUS_CONCURRENT_OPERATION);
+        assertThat(callback1.response).isNull();
+    }
+
+    @Test
+    public void testSwitchUser_halReturnedInvalidStatus() throws Exception {
+        // TODO(b/150419600): use helper method to convert prop value to proper req
+        VehiclePropValue propResponse = new VehiclePropValue();
+        propResponse.prop = SWITCH_USER;
+        propResponse.value.int32Values.add(REQUEST_ID_PLACE_HOLDER);
+        propResponse.value.int32Values.add(SwitchUserMessageType.VEHICLE_RESPONSE);
+        propResponse.value.int32Values.add(/*status =*/ 110); // an invalid status
+
+        AtomicReference<VehiclePropValue> reqCaptor = replySetPropertyWithOnChangeEvent(
+                SWITCH_USER, propResponse, /* rightRequestId= */ true);
+
+        GenericHalCallback<SwitchUserResponse> callback = new GenericHalCallback<>(
+                CALLBACK_TIMEOUT_SUCCESS);
+        mUserHalService.switchUser(mUser10, TIMEOUT_MS, mUsersInfo, callback);
+
+        callback.assertCalled();
+
+        // Make sure the arguments were properly converted
+        assertSwitchUserSetRequest(reqCaptor.get(), SwitchUserMessageType.ANDROID_SWITCH, mUser10);
+
+        // Assert response
+        assertCallbackStatus(callback, HalCallback.STATUS_WRONG_HAL_RESPONSE);
+        assertThat(callback.response).isNull();
+    }
+
     /**
      * Asserts the given {@link UsersInfo} is properly represented in the {@link VehiclePropValue}.
      *
@@ -455,7 +655,17 @@
         assertUsersInfo(req, mUsersInfo, 2);
     }
 
-    private void assertCallbackStatus(GenericHalCallback<InitialUserInfoResponse> callback,
+    private void assertSwitchUserSetRequest(VehiclePropValue req, int messageType,
+            UserInfo targetUserInfo) {
+        assertThat(req.value.int32Values.get(1)).isEqualTo(messageType);
+        assertWithMessage("targetuser.id mismatch").that(req.value.int32Values.get(2))
+                .isEqualTo(targetUserInfo.userId);
+        assertWithMessage("targetuser.flags mismatch").that(req.value.int32Values.get(3))
+                .isEqualTo(targetUserInfo.flags);
+        assertUsersInfo(req, mUsersInfo, 4);
+    }
+
+    private void assertCallbackStatus(GenericHalCallback callback,
             int expectedStatus) {
         int actualStatus = callback.status;
         if (actualStatus == expectedStatus) return;