/* * Copyright (C) 2021 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.hiddenapitest; import static android.car.test.util.UserTestingHelper.setMaxSupportedUsers; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.compatibility.common.util.ShellUtils.runShellCommand; import static com.google.common.truth.Truth.assertWithMessage; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.car.Car; import android.car.CarOccupantZoneManager; import android.car.SyncResultCallback; import android.car.extendedapitest.testbase.CarApiTestBase; import android.car.test.util.AndroidHelper; import android.car.test.util.UserTestingHelper; import android.car.testapi.BlockingUserLifecycleListener; import android.car.user.CarUserManager; import android.car.user.UserCreationRequest; import android.car.user.UserCreationResult; import android.car.user.UserRemovalRequest; import android.car.user.UserStartRequest; import android.car.user.UserStopRequest; import android.car.user.UserSwitchRequest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; import android.os.Bundle; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import org.junit.After; import org.junit.AssumptionViolatedException; import org.junit.Before; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Base class for tests that deal with multi user operations (creation, switch, etc) * */ abstract class CarMultiUserTestBase extends CarApiTestBase { private static final String TAG = CarMultiUserTestBase.class.getSimpleName(); private static final long REMOVE_USER_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(30_000); private static final long SWITCH_USER_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(30_000); private static final String NEW_USER_NAME_PREFIX = "CarApiTest."; private static final int sMaxNumberUsersBefore = UserManager.getMaxSupportedUsers(); private static boolean sChangedMaxNumberUsers; protected CarOccupantZoneManager mCarOccupantZoneManager; protected CarUserManager mCarUserManager; protected UserManager mUserManager; /** * Current user before the test runs. */ private UserInfo mInitialUser; private final CountDownLatch mUserRemoveLatch = new CountDownLatch(1); private final List mUsersToRemove = new ArrayList<>(); private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received a broadcast: " + AndroidHelper.toString(intent)); mUserRemoveLatch.countDown(); } }; // Guard to avoid test failure on @After when @Before failed (as it would hide the real issue) private boolean mSetupFinished; @Before public final void setMultiUserFixtures() throws Exception { Log.d(TAG, "setMultiUserFixtures() for " + getTestName()); mCarOccupantZoneManager = getCarService(Car.CAR_OCCUPANT_ZONE_SERVICE); mCarUserManager = getCarService(Car.CAR_USER_SERVICE); mUserManager = getContext().getSystemService(UserManager.class); IntentFilter filter = new IntentFilter(Intent.ACTION_USER_REMOVED); getContext().registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED); Log.d(TAG, "Registered a broadcast receiver: " + mReceiver + " with filter: " + filter); List users = mUserManager.getAliveUsers(); // Set current user int currentUserId = getCurrentUserId(); Log.d(TAG, "Multi-user state on " + getTestName() + ": currentUser=" + currentUserId + ", aliveUsers=" + users); for (UserInfo user : users) { if (user.id == currentUserId) { mInitialUser = user; break; } } assertWithMessage("user for currentId %s", currentUserId).that(mInitialUser).isNotNull(); // Make sure current user is not a left-over from previous test if (isUserCreatedByTheseTests(mInitialUser)) { UserInfo properUser = null; for (UserInfo user : users) { if (user.id != UserHandle.USER_SYSTEM && !isUserCreatedByTheseTests(user)) { properUser = user; break; } } assertWithMessage("found a proper user to switch from %s", mInitialUser.toFullString()) .that(properUser).isNotNull(); Log.i(TAG, "Current user on start of " + getTestName() + " is a dangling user: " + mInitialUser.toFullString() + "; switching to " + properUser.toFullString()); switchUser(properUser.id); mInitialUser = properUser; } // Remove dangling users from previous tests for (UserInfo user : users) { if (!isUserCreatedByTheseTests(user)) continue; Log.e(TAG, "Removing dangling user " + user.toFullString() + " on @Before method of " + getTestName()); boolean removed = mUserManager.removeUser(user.id); if (!removed) { Log.e(TAG, "user " + user.toFullString() + " was not removed"); } } Log.d(TAG, "setMultiUserFixtures(): Saul Goodman, setting mSetupFinished to true"); mSetupFinished = true; } @After public final void cleanupUserState() throws Exception { try { if (!mSetupFinished) { Log.w(TAG, "skipping cleanupUserState() because mSetupFinished is false"); return; } getContext().unregisterReceiver(mReceiver); Log.d(TAG, "Unregistered a broadcast receiver: " + mReceiver); int currentUserId = getCurrentUserId(); int initialUserId = mInitialUser.id; if (currentUserId != initialUserId) { Log.i(TAG, "Wrong current userId at the end of " + getTestName() + ": " + currentUserId + "; switching back to " + initialUserId); switchUser(initialUserId); } if (!mUsersToRemove.isEmpty()) { Log.i(TAG, "removing users at end of " + getTestName() + ": " + mUsersToRemove); for (Integer userId : mUsersToRemove) { if (hasUser(userId)) { mUserManager.removeUser(userId); } } } else { Log.i(TAG, "no user to remove at end of " + getTestName()); } } catch (Exception e) { // Must catch otherwise it would be the test failure, which could hide the real issue Log.e(TAG, "Caught exception on " + getTestName() + " disconnectCarAndCleanupUserState()", e); } } protected static void setupMaxNumberOfUsers(int requiredUsers) { if (sMaxNumberUsersBefore < requiredUsers) { sChangedMaxNumberUsers = true; Log.i(TAG, "Increasing maximizing number of users from " + sMaxNumberUsersBefore + " to " + requiredUsers); setMaxSupportedUsers(requiredUsers); } } protected static void restoreMaxNumberOfUsers() { if (sChangedMaxNumberUsers) { Log.i(TAG, "Restoring maximum number of users to " + sMaxNumberUsersBefore); setMaxSupportedUsers(sMaxNumberUsersBefore); } } @UserIdInt protected int getCurrentUserId() { return ActivityManager.getCurrentUser(); } @NonNull protected UserInfo createUser() throws Exception { return createUser("NonGuest"); } @NonNull protected UserInfo createUser(String name) throws Exception { return createUser(name, /* isGuest= */ false); } @NonNull protected UserInfo createGuest() throws Exception { return createGuest("Guest"); } @NonNull protected UserInfo createGuest(String name) throws Exception { return createUser(name, /* isGuest= */ true); } @NonNull private UserInfo createUser(@Nullable String name, boolean isGuest) throws Exception { name = getNewUserName(name); Log.d(TAG, "Creating new " + (isGuest ? "guest" : "user") + " with name '" + name + "' using CarUserManager"); assertCanAddUser(); UserCreationRequest.Builder userCreationRequestBuilder = new UserCreationRequest.Builder(); if (isGuest) { userCreationRequestBuilder.setGuest(); } SyncResultCallback userCreationResultCallback = new SyncResultCallback<>(); mCarUserManager.createUser(userCreationRequestBuilder.setName(name).build(), Runnable::run, userCreationResultCallback); UserCreationResult result = userCreationResultCallback.get( DEFAULT_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); Log.d(TAG, "result: " + result); assertWithMessage("user creation result (waited for %sms)", DEFAULT_WAIT_TIMEOUT_MS) .that(result).isNotNull(); assertWithMessage("user creation result (%s) success for user named %s", result, name) .that(result.isSuccess()).isTrue(); UserHandle user = result.getUser(); assertWithMessage("user on result %s", result).that(user).isNotNull(); mUsersToRemove.add(user.getIdentifier()); assertWithMessage("new user %s is guest", user.toString()) .that(mUserManager.isGuestUser(user.getIdentifier())) .isEqualTo(isGuest); return mUserManager.getUserInfo(result.getUser().getIdentifier()); } private String getNewUserName(String name) { StringBuilder newName = new StringBuilder(NEW_USER_NAME_PREFIX).append(getTestName()); if (name != null) { newName.append('.').append(name); } return newName.toString(); } protected void assertCanAddUser() { Bundle restrictions = mUserManager.getUserRestrictions(); Log.d(TAG, "Restrictions for user " + getContext().getUser() + ": " + AndroidHelper.toString(restrictions)); assertWithMessage("%s restriction", UserManager.DISALLOW_ADD_USER) .that(restrictions.getBoolean(UserManager.DISALLOW_ADD_USER, false)).isFalse(); } protected void assertInitialUserIsAdmin() { assertWithMessage("initial user (%s) is admin", mInitialUser.toFullString()) .that(mInitialUser.isAdmin()).isTrue(); } protected void waitForUserRemoval(@UserIdInt int userId) throws Exception { boolean result = mUserRemoveLatch.await(REMOVE_USER_TIMEOUT_MS, TimeUnit.MILLISECONDS); assertWithMessage("User %s removed in %sms", userId, REMOVE_USER_TIMEOUT_MS) .that(result) .isTrue(); } protected void switchUser(@UserIdInt int userId) throws Exception { boolean waitForUserSwitchToComplete = true; // If current user is the target user, no life cycle event is expected. if (getCurrentUserId() == userId) waitForUserSwitchToComplete = false; Log.d(TAG, "registering listener for user switching"); BlockingUserLifecycleListener listener = BlockingUserLifecycleListener .forSpecificEvents() .forUser(userId) .setTimeout(SWITCH_USER_TIMEOUT_MS) .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_SWITCHING) .build(); mCarUserManager.addListener(Runnable::run, listener); try { Log.i(TAG, "Switching to user " + userId + " using CarUserManager"); mCarUserManager.switchUser(new UserSwitchRequest.Builder( UserHandle.of(userId)).build(), Runnable::run, response -> { Log.d(TAG, "result: " + response); assertWithMessage("User %s switched in %sms. Result: %s", userId, SWITCH_USER_TIMEOUT_MS, response).that(response.isSuccess()).isTrue(); } ); if (waitForUserSwitchToComplete) { listener.waitForEvents(); } } finally { mCarUserManager.removeListener(listener); } Log.d(TAG, "User switch complete. User id: " + userId); } protected void removeUser(@UserIdInt int userId) throws Exception { Log.d(TAG, "Removing user " + userId); mCarUserManager.removeUser(new UserRemovalRequest.Builder( UserHandle.of(userId)).build(), Runnable::run, response -> { Log.d(TAG, "result: " + response); assertWithMessage("User %s removed. Result: %s", userId, response) .that(response.isSuccess()).isTrue(); } ); } protected void startUserInBackgroundOnSecondaryDisplay(@UserIdInt int userId, int displayId) throws Exception { Log.i(TAG, "Starting background user " + userId + " on display " + displayId); UserStartRequest request = new UserStartRequest.Builder(UserHandle.of(userId)) .setDisplayId(displayId).build(); mCarUserManager.startUser(request, Runnable::run, response -> assertWithMessage("startUser success for user %s on display %s", userId, displayId).that(response.isSuccess()).isTrue()); } protected void forceStopUser(@UserIdInt int userId) throws Exception { Log.i(TAG, "Force-stopping user " + userId); UserStopRequest request = new UserStopRequest.Builder(UserHandle.of(userId)).setForce().build(); mCarUserManager.stopUser(request, Runnable::run, response -> assertWithMessage("stopUser success for user %s", userId) .that(response.isSuccess()).isTrue()); } @Nullable protected UserInfo getUser(@UserIdInt int id) { List list = mUserManager.getUsers(); for (UserInfo user : list) { if (user.id == id) { return user; } } return null; } protected boolean hasUser(@UserIdInt int id) { return getUser(id) != null; } protected void setSystemProperty(String property, String value) { String oldValue = SystemProperties.get(property); Log.d(TAG, "Setting system prop " + property + " from '" + oldValue + "' to '" + value + "'"); // NOTE: must use Shell command as SystemProperties.set() requires SELinux permission check // (so invokeWithShellPermissions() would not be enough) runShellCommand("setprop %s %s", property, value); Log.v(TAG, "Set: " + SystemProperties.get(property)); } protected void assertUserInfo(UserInfo actualUser, UserInfo expectedUser) { assertWithMessage("Wrong id for user %s", actualUser.toFullString()) .that(actualUser.id).isEqualTo(expectedUser.id); assertWithMessage("Wrong name for user %s", actualUser.toFullString()) .that(actualUser.name).isEqualTo(expectedUser.name); assertWithMessage("Wrong type for user %s", actualUser.toFullString()) .that(actualUser.userType).isEqualTo(expectedUser.userType); assertWithMessage("Wrong flags for user %s", actualUser.toFullString()) .that(actualUser.flags).isEqualTo(expectedUser.flags); } private static boolean isUserCreatedByTheseTests(UserInfo user) { return user.name != null && user.name.startsWith(NEW_USER_NAME_PREFIX); } /** * Checks if the target device supports MUMD (multi-user multi-display). * @throws AssumptionViolatedException if the device does not support MUMD. */ protected static void requireMumd() { UserTestingHelper.requireMumd(getTargetContext()); } /** * Returns a secondary display that is available to start a background user on. * * @return the id of a secondary display that is not assigned to any user, if any. * @throws IllegalStateException when there is no secondary display available. */ protected int getDisplayForStartingBackgroundUser() { return UserTestingHelper.getDisplayForStartingBackgroundUser( getTargetContext(), mCarOccupantZoneManager); } private static Context getTargetContext() { return getInstrumentation().getTargetContext(); } }