Implemented CarService.getInitialUserInfo().

Test: atest CarServiceUnitTest#testGetUserInfo*

Bug: 146207078
Change-Id: Ib708f48a9ea1bd45ffe7329f7386e760bba16a2a
diff --git a/car-lib/src/android/car/ICarUserService.aidl b/car-lib/src/android/car/ICarUserService.aidl
index 1ff1522..1561ad9 100644
--- a/car-lib/src/android/car/ICarUserService.aidl
+++ b/car-lib/src/android/car/ICarUserService.aidl
@@ -30,4 +30,6 @@
     boolean stopPassenger(int passengerId);
     oneway void setLifecycleListenerForUid(in IResultReceiver listener);
     oneway void resetLifecycleListenerForUid();
+    oneway void getInitialUserInfo(int requestType, int timeoutMs, in UserInfo[] existingUsers,
+        int currentUserId, in IResultReceiver receiver);
 }
diff --git a/service/src/com/android/car/user/CarUserService.java b/service/src/com/android/car/user/CarUserService.java
index e2f21b5..5ec0933 100644
--- a/service/src/com/android/car/user/CarUserService.java
+++ b/service/src/com/android/car/user/CarUserService.java
@@ -34,6 +34,9 @@
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.graphics.Bitmap;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
+import android.hardware.automotive.vehicle.V2_0.UserFlags;
+import android.hardware.automotive.vehicle.V2_0.UsersInfo;
 import android.location.LocationManager;
 import android.os.Binder;
 import android.os.Bundle;
@@ -52,6 +55,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.Preconditions;
 import com.android.internal.util.UserIcons;
 
 import java.io.PrintWriter;
@@ -74,6 +78,12 @@
  */
 public final class CarUserService extends ICarUserService.Stub implements CarServiceBase {
 
+    /** Extra used to represent a user id in a {@link IResultReceiver} response. */
+    public static final String BUNDLE_USER_ID = "user.id";
+    /** Extra used to represent user flags in a {@link IResultReceiver} response. */
+    public static final String BUNDLE_USER_FLAGS = "user.flags";
+    /** Extra used to represent a user name in a {@link IResultReceiver} response. */
+    public static final String BUNDLE_USER_NAME = "user.name";
 
     private final Context mContext;
     private final CarUserManagerHelper mCarUserManagerHelper;
@@ -466,6 +476,75 @@
         mLifecycleListeners.remove(uid);
     }
 
+    @Override
+    public void getInitialUserInfo(int requestType, int timeoutMs,
+            @NonNull UserInfo[] existingUsers, @UserIdInt int currentUserId,
+            @NonNull IResultReceiver receiver) {
+
+        // TODO(b/144120654): use helper to generate USerInfo
+        UsersInfo usersInfo = new UsersInfo();
+        usersInfo.numberUsers = existingUsers.length;
+        boolean foundCurrentUser = false;
+        for (int i = 0; i < existingUsers.length; i++) {
+            UserInfo user = existingUsers[i];
+            android.hardware.automotive.vehicle.V2_0.UserInfo halUser =
+                    new android.hardware.automotive.vehicle.V2_0.UserInfo();
+            halUser.userId = user.id;
+            int flags = UserFlags.NONE;
+            if (user.id == UserHandle.USER_SYSTEM) {
+                flags |= UserFlags.SYSTEM;
+            }
+            if (user.isAdmin()) {
+                flags |= UserFlags.ADMIN;
+            }
+            if (user.isGuest()) {
+                flags |= UserFlags.GUEST;
+            }
+            if (user.isEphemeral()) {
+                flags |= UserFlags.EPHEMERAL;
+            }
+            halUser.flags = flags;
+            usersInfo.existingUsers.add(halUser);
+            if (user.id == currentUserId) {
+                // TODO(b/144120654): unit test for when it isn't there (on helper method), or
+                // change signature so the currentUser argument is a UserInfo)
+                foundCurrentUser = true;
+                usersInfo.currentUser.userId = currentUserId;
+                usersInfo.currentUser.flags = flags;
+            }
+        }
+        Preconditions.checkArgument(foundCurrentUser,
+                "no user with id " + currentUserId + " on " + existingUsers);
+
+        mHal.getInitialUserInfo(requestType, timeoutMs, usersInfo, (status, resp) -> {
+            try {
+                Bundle resultData = null;
+                if (resp != null) {
+                    switch (resp.action) {
+                        case InitialUserInfoResponseAction.SWITCH:
+                            resultData = new Bundle();
+                            resultData.putInt(BUNDLE_USER_ID, resp.userToSwitchOrCreate.userId);
+                            break;
+                        case InitialUserInfoResponseAction.CREATE:
+                            resultData = new Bundle();
+                            resultData.putInt(BUNDLE_USER_FLAGS, resp.userToSwitchOrCreate.flags);
+                            resultData.putString(BUNDLE_USER_NAME, resp.userNameToCreate);
+                            break;
+                        case InitialUserInfoResponseAction.DEFAULT:
+                            // do nothing
+                            break;
+                        default:
+                            // That's ok, it will be the same as DEFAULT...
+                            Log.w(TAG_USER, "invalid response action on " + resp);
+                    }
+                }
+                receiver.send(status, resultData);
+            } catch (RemoteException e) {
+                Log.w(TAG_USER, "Could not send result back to receiver", e);
+            }
+        });
+    }
+
     /** Returns whether the given user is a system user. */
     private static boolean isSystemUser(@UserIdInt int userId) {
         return userId == UserHandle.USER_SYSTEM;
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 0e36211..3124a60 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
@@ -22,6 +22,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -29,6 +30,8 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -46,18 +49,28 @@
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoRequestType;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponse;
+import android.hardware.automotive.vehicle.V2_0.InitialUserInfoResponseAction;
+import android.hardware.automotive.vehicle.V2_0.UserFlags;
+import android.hardware.automotive.vehicle.V2_0.UsersInfo;
 import android.location.LocationManager;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.util.Log;
 import android.util.SparseArray;
 
+import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
 
 import com.android.car.hal.UserHalService;
+import com.android.car.hal.UserHalService.HalCallback;
 import com.android.internal.R;
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.Preconditions;
 
 import org.junit.After;
 import org.junit.Before;
@@ -73,6 +86,9 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 /**
  * This class contains unit tests for the {@link CarUserService}.
  *
@@ -87,6 +103,8 @@
  */
 @RunWith(MockitoJUnitRunner.class)
 public class CarUserServiceTest {
+
+    private static final String TAG = CarUserServiceTest.class.getSimpleName();
     private static final int NO_USER_INFO_FLAGS = 0;
 
     @Mock private Context mMockContext;
@@ -104,6 +122,24 @@
     private boolean mUser0TaskExecuted;
     private FakeCarOccupantZoneService mFakeCarOccupantZoneService;
 
+    private final int mGetUserInfoRequestType = InitialUserInfoRequestType.COLD_BOOT;
+    private final int mAsyncCallTimeoutMs = 100;
+    private final BlockingResultReceiver mReceiver =
+            new BlockingResultReceiver(mAsyncCallTimeoutMs);
+    private final InitialUserInfoResponse mGetUserInfoResponse = new InitialUserInfoResponse();
+
+    private final @NonNull UserInfo mSystemUser = UserInfoBuilder.newSystemUserInfo();
+    private final @NonNull UserInfo mAdminUser = new UserInfoBuilder(10)
+            .setAdmin(true)
+            .build();
+    private final @NonNull UserInfo mGuestUser = new UserInfoBuilder(11)
+            .setGuest(true)
+            .setEphemeral(true)
+            .build();
+    private final UserInfo[] mExistingUsers = new UserInfo[] {
+            mSystemUser, mAdminUser, mGuestUser
+    };
+
     /**
      * Initialize all of the objects with the @Mock annotation.
      */
@@ -542,6 +578,146 @@
         }
     }
 
+    @Test
+    public void testGetUserInfo_defaultResponse() throws Exception {
+        int currentUserId = mAdminUser.id;
+
+        mGetUserInfoResponse.action = InitialUserInfoResponseAction.DEFAULT;
+        mockGetInitialInfo(currentUserId, mGetUserInfoResponse);
+
+        mCarUserService.getInitialUserInfo(mGetUserInfoRequestType, mAsyncCallTimeoutMs,
+                mExistingUsers, currentUserId, mReceiver);
+
+        assertThat(mReceiver.getResultCode()).isEqualTo(HalCallback.STATUS_OK);
+        assertThat(mReceiver.getResultData()).isNull();
+    }
+
+    @Test
+    public void testGetUserInfo_switchUserResponse() throws Exception {
+        int currentUserId = mAdminUser.id;
+        int switchUserId = mGuestUser.id;
+
+        mGetUserInfoResponse.action = InitialUserInfoResponseAction.SWITCH;
+        mGetUserInfoResponse.userToSwitchOrCreate.userId = switchUserId;
+        mockGetInitialInfo(currentUserId, mGetUserInfoResponse);
+
+        mCarUserService.getInitialUserInfo(mGetUserInfoRequestType, mAsyncCallTimeoutMs,
+                mExistingUsers, currentUserId, mReceiver);
+
+        assertThat(mReceiver.getResultCode()).isEqualTo(HalCallback.STATUS_OK);
+        Bundle resultData = mReceiver.getResultData();
+        assertThat(resultData).isNotNull();
+        assertUserId(resultData, switchUserId);
+        assertNoUserFlags(resultData);
+        assertNoUserName(resultData);
+    }
+
+    @Test
+    public void testGetUserInfo_createUserResponse() throws Exception {
+        int currentUserId = mAdminUser.id;
+        int newUserFlags = 42;
+        String newUserName = "TheDude";
+
+        mGetUserInfoResponse.action = InitialUserInfoResponseAction.CREATE;
+        mGetUserInfoResponse.userToSwitchOrCreate.flags = newUserFlags;
+        mGetUserInfoResponse.userNameToCreate = newUserName;
+        mockGetInitialInfo(currentUserId, mGetUserInfoResponse);
+
+        mCarUserService.getInitialUserInfo(mGetUserInfoRequestType, mAsyncCallTimeoutMs,
+                mExistingUsers, currentUserId, mReceiver);
+
+        assertThat(mReceiver.getResultCode()).isEqualTo(HalCallback.STATUS_OK);
+        Bundle resultData = mReceiver.getResultData();
+        assertThat(resultData).isNotNull();
+        assertNoUserId(resultData);
+        assertUserFlags(resultData, newUserFlags);
+        assertUserName(resultData, newUserName);
+    }
+
+    private void mockGetInitialInfo(@UserIdInt int currentUserId,
+            @NonNull InitialUserInfoResponse response) {
+        UsersInfo usersInfo = newUsersInfo(currentUserId);
+        doAnswer((invocation) -> {
+            Log.d(TAG, "Answering " + invocation + " with " + response);
+            @SuppressWarnings("unchecked")
+            HalCallback<InitialUserInfoResponse> callback =
+                    (HalCallback<InitialUserInfoResponse>) invocation.getArguments()[3];
+            callback.onResponse(HalCallback.STATUS_OK, response);
+            return null;
+        }).when(mUserHal).getInitialUserInfo(eq(mGetUserInfoRequestType), eq(mAsyncCallTimeoutMs),
+                eq(usersInfo), notNull());
+    }
+
+    @NonNull
+    private UsersInfo newUsersInfo(@UserIdInt int currentUserId) {
+        UsersInfo infos = new UsersInfo();
+        infos.numberUsers = mExistingUsers.length;
+        boolean foundCurrentUser = false;
+        for (UserInfo info : mExistingUsers) {
+            android.hardware.automotive.vehicle.V2_0.UserInfo existingUser =
+                    new android.hardware.automotive.vehicle.V2_0.UserInfo();
+            int flags = UserFlags.NONE;
+            if (info.id == UserHandle.USER_SYSTEM) {
+                flags |= UserFlags.SYSTEM;
+            }
+            if (info.isAdmin()) {
+                flags |= UserFlags.ADMIN;
+            }
+            if (info.isGuest()) {
+                flags |= UserFlags.GUEST;
+            }
+            if (info.isEphemeral()) {
+                flags |= UserFlags.EPHEMERAL;
+            }
+            existingUser.userId = info.id;
+            existingUser.flags = flags;
+            if (info.id == currentUserId) {
+                foundCurrentUser = true;
+                infos.currentUser.userId = info.id;
+                infos.currentUser.flags = flags;
+            }
+            infos.existingUsers.add(existingUser);
+        }
+        Preconditions.checkArgument(foundCurrentUser,
+                "no user with id " + currentUserId + " on " + mExistingUsers);
+        return infos;
+    }
+
+    private void assertUserId(@NonNull Bundle resultData, int expectedUserId) {
+        int actualUserId = resultData.getInt(CarUserService.BUNDLE_USER_ID);
+        assertWithMessage("wrong user id on bundle extra %s", CarUserService.BUNDLE_USER_ID)
+                .that(actualUserId).isEqualTo(expectedUserId);
+    }
+
+    private void assertNoUserId(@NonNull Bundle resultData) {
+        assertNoExtra(resultData, CarUserService.BUNDLE_USER_ID);
+    }
+
+    private void assertUserFlags(@NonNull Bundle resultData, int expectedUserFlags) {
+        int actualUserFlags = resultData.getInt(CarUserService.BUNDLE_USER_FLAGS);
+        assertWithMessage("wrong user flags on bundle extra %s", CarUserService.BUNDLE_USER_FLAGS)
+                .that(actualUserFlags).isEqualTo(expectedUserFlags);
+    }
+
+    private void assertNoUserFlags(@NonNull Bundle resultData) {
+        assertNoExtra(resultData, CarUserService.BUNDLE_USER_FLAGS);
+    }
+
+    private void assertUserName(@NonNull Bundle resultData, @NonNull String expectedName) {
+        String actualName = resultData.getString(CarUserService.BUNDLE_USER_NAME);
+        assertWithMessage("wrong user name on bundle extra %s",
+                CarUserService.BUNDLE_USER_FLAGS).that(actualName).isEqualTo(expectedName);
+    }
+
+    private void assertNoUserName(@NonNull Bundle resultData) {
+        assertNoExtra(resultData, CarUserService.BUNDLE_USER_NAME);
+    }
+
+    private void assertNoExtra(@NonNull Bundle resultData, @NonNull String extra) {
+        Object value = resultData.get(extra);
+        assertWithMessage("should not have extra %s", extra).that(value).isNull();
+    }
+
     static final class FakeCarOccupantZoneService {
         private final SparseArray<Integer> mZoneUserMap = new SparseArray<Integer>();
         private final CarUserService.ZoneUserBindingHelper mZoneUserBindigHelper =
@@ -585,11 +761,6 @@
     }
 
 
-    private void putSettingsInt(String key, int value) {
-        Settings.Global.putInt(InstrumentationRegistry.getTargetContext().getContentResolver(),
-                key, value);
-    }
-
     // TODO(b/148403316): Refactor to use common fake settings provider
     private void mockSettingsGlobal() {
         when(Settings.Global.putInt(any(), eq(CarSettings.Global.DEFAULT_USER_RESTRICTIONS_SET),
@@ -603,9 +774,160 @@
         );
     }
 
+    private void putSettingsInt(String key, int value) {
+        Settings.Global.putInt(InstrumentationRegistry.getTargetContext().getContentResolver(),
+                key, value);
+    }
+
     private int getSettingsInt(String key) {
         return Settings.Global.getInt(
                 InstrumentationRegistry.getTargetContext().getContentResolver(),
                 key, /* default= */ 0);
     }
+
+    // TODO(b/149099817): move stuff below to common code
+
+    /**
+     * Builder for {@link UserInfo} objects.
+     *
+     */
+    public static final class UserInfoBuilder {
+
+        @UserIdInt
+        private final int mUserId;
+
+        @Nullable
+        private String mName;
+
+        private boolean mGuest;
+        private boolean mEphemeral;
+        private boolean mAdmin;
+
+        /**
+         * Default constructor.
+         */
+        public UserInfoBuilder(@UserIdInt int userId) {
+            mUserId = userId;
+        }
+
+        /**
+         * Sets the user name.
+         */
+        @NonNull
+        public UserInfoBuilder setName(@Nullable String name) {
+            mName = name;
+            return this;
+        }
+
+        /**
+         * Sets whether the user is a guest.
+         */
+        @NonNull
+        public UserInfoBuilder setGuest(boolean guest) {
+            mGuest = guest;
+            return this;
+        }
+
+        /**
+         * Sets whether the user is ephemeral.
+         */
+        @NonNull
+        public UserInfoBuilder setEphemeral(boolean ephemeral) {
+            mEphemeral = ephemeral;
+            return this;
+        }
+
+        /**
+         * Sets whether the user is an admin.
+         */
+        @NonNull
+        public UserInfoBuilder setAdmin(boolean admin) {
+            mAdmin = admin;
+            return this;
+        }
+
+        /**
+         * Creates a new {@link UserInfo}.
+         */
+        @NonNull
+        public UserInfo build() {
+            int flags = 0;
+            if (mEphemeral) {
+                flags |= UserInfo.FLAG_EPHEMERAL;
+            }
+            if (mAdmin) {
+                flags |= UserInfo.FLAG_ADMIN;
+            }
+            UserInfo info = new UserInfo(mUserId, mName, flags);
+            if (mGuest) {
+                info.userType = UserManager.USER_TYPE_FULL_GUEST;
+            }
+            return info;
+        }
+
+        /**
+         * Creates a new {@link UserInfo} for a system user.
+         */
+        @NonNull
+        public static UserInfo newSystemUserInfo() {
+            UserInfo info = new UserInfo();
+            info.id = UserHandle.USER_SYSTEM;
+            return info;
+        }
+    }
+
+    /**
+     * Implementation of {@link IResultReceiver} that blocks waiting for the result.
+     */
+    public static final class BlockingResultReceiver extends IResultReceiver.Stub {
+
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private final long mTimeoutMs;
+
+        private int mResultCode;
+        @Nullable private Bundle mResultData;
+
+        /**
+         * Default constructor.
+         *
+         * @param timeoutMs how long to wait for before failing.
+         */
+        public BlockingResultReceiver(long timeoutMs) {
+            mTimeoutMs = timeoutMs;
+        }
+
+        @Override
+        public void send(int resultCode, Bundle resultData) {
+            Log.d(TAG, "send() received: code=" + resultCode + ", data=" + resultData + ", count="
+                    + mLatch.getCount());
+            Preconditions.checkState(mLatch.getCount() == 1,
+                    "send() already called (code=" + mResultCode + ", data=" + mResultData);
+            mResultCode = resultCode;
+            mResultData = resultData;
+            mLatch.countDown();
+        }
+
+        private void assertCalled() throws InterruptedException {
+            boolean called = mLatch.await(mTimeoutMs, TimeUnit.MILLISECONDS);
+            Log.d(TAG, "assertCalled(): " + called);
+            assertWithMessage("receiver not called in %sms", mTimeoutMs).that(called).isTrue();
+        }
+
+        /**
+         * Gets the {@code resultCode} or fails if it times out before {@code send()} is called.
+         */
+        public int getResultCode() throws InterruptedException {
+            assertCalled();
+            return mResultCode;
+        }
+
+        /**
+         * Gets the {@code resultData} or fails if it times out before {@code send()} is called.
+         */
+        @Nullable
+        public Bundle getResultData() throws InterruptedException {
+            assertCalled();
+            return mResultData;
+        }
+    }
 }