1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.platform.helpers; 17 18 import android.annotation.Nullable; 19 import android.app.ActivityManager; 20 import android.car.Car; 21 import android.car.CarOccupantZoneManager; 22 import android.car.SyncResultCallback; 23 import android.car.user.CarUserManager; 24 import android.car.user.CarUserManager.UserLifecycleListener; 25 import android.car.user.UserCreationRequest; 26 import android.car.user.UserCreationResult; 27 import android.car.user.UserStartResponse; 28 import android.car.user.UserStartRequest; 29 import android.car.user.UserSwitchResult; 30 import android.car.user.UserStopRequest; 31 import android.car.user.UserStopResponse; 32 import android.car.util.concurrent.AsyncFuture; 33 import android.content.Context; 34 import android.content.pm.UserInfo; 35 import android.os.Build; 36 import android.os.SystemClock; 37 import android.os.UserManager; 38 import android.support.test.uiautomator.UiDevice; 39 import android.util.Log; 40 41 import androidx.test.InstrumentationRegistry; 42 43 import com.android.compatibility.common.util.SystemUtil; 44 45 import java.io.IOException; 46 import java.util.concurrent.CountDownLatch; 47 import java.util.concurrent.TimeUnit; 48 49 /** 50 * Helper class that is used by integration test only. It is wrapping around exiting platform APIs 51 * {@link CarUserManager}, {@link UserManager} to expose functions for user switch end-to-end tests. 52 */ 53 public class MultiUserHelper { 54 private static final int CREATE_USER_TIMEOUT_MS = 20_000; 55 private static final String LOG_TAG = MultiUserHelper.class.getSimpleName(); 56 57 /** For testing purpose we allow a wide range of switching time. */ 58 private static final int USER_SWITCH_TIMEOUT_SECOND = 300; 59 60 private static final String CREATE_USER_COMMAND = "cmd car_service create-user "; 61 62 private static MultiUserHelper sMultiUserHelper; 63 private CarUserManager mCarUserManager; 64 private UserManager mUserManager; 65 private ActivityManager mActivityManager; 66 private CarOccupantZoneManager mCarOccupantZoneManager; 67 MultiUserHelper()68 private MultiUserHelper() { 69 Context context = InstrumentationRegistry.getTargetContext(); 70 mUserManager = UserManager.get(context); 71 Car car = Car.createCar(context); 72 mCarUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE); 73 mActivityManager = context.getSystemService(ActivityManager.class); 74 mCarOccupantZoneManager = car.getCarManager(CarOccupantZoneManager.class); 75 } 76 77 /** 78 * It will always be used as a singleton class 79 * 80 * @return MultiUserHelper instance 81 */ getInstance()82 public static MultiUserHelper getInstance() { 83 if (sMultiUserHelper == null) { 84 sMultiUserHelper = new MultiUserHelper(); 85 } 86 return sMultiUserHelper; 87 } 88 89 /** 90 * Creates a user if it does not exist already. 91 * 92 * @param name the name of the user or guest 93 * @param isGuestUser true if want to create a guest, otherwise create a regular user 94 * @return User Id for newly created user or existing user if it already exists 95 */ createUserIfDoesNotExist(String name, boolean isGuestUser)96 public int createUserIfDoesNotExist(String name, boolean isGuestUser) throws Exception { 97 UserInfo userInfo = getUserByName(name); 98 if (userInfo != null) { 99 return userInfo.id; 100 } 101 return createUser(name, isGuestUser); 102 } 103 104 /** 105 * Creates a regular user or guest 106 * 107 * @param name the name of the user or guest 108 * @param isGuestUser true if want to create a guest, otherwise create a regular user 109 * @return User Id for newly created user 110 */ createUser(String name, boolean isGuestUser)111 public int createUser(String name, boolean isGuestUser) throws Exception { 112 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 113 Log.d( 114 LOG_TAG, 115 "An Android version earlier than UDC is detected. Using the shell for user " 116 + "creation"); 117 return createUserUsingShell(name, isGuestUser); 118 } 119 Log.d( 120 LOG_TAG, 121 "Creating new " 122 + (isGuestUser ? "guest" : "user") 123 + " with name '" 124 + name 125 + "' using CarUserManager"); 126 SyncResultCallback<UserCreationResult> userCreationResultCallback = 127 new SyncResultCallback<>(); 128 if (isGuestUser) { 129 // Create a new guest 130 mCarUserManager.createUser( 131 new UserCreationRequest.Builder().setName(name).setGuest().build(), 132 Runnable::run, 133 userCreationResultCallback); 134 } else { 135 // Create a new user 136 mCarUserManager.createUser( 137 new UserCreationRequest.Builder().setName(name).build(), 138 Runnable::run, 139 userCreationResultCallback); 140 } 141 UserCreationResult result = 142 userCreationResultCallback.get(CREATE_USER_TIMEOUT_MS, TimeUnit.MILLISECONDS); 143 if (!result.isSuccess()) { 144 throw new Exception(String.format("The user was not created successfully: %s", result)); 145 } 146 return result.getUser().getIdentifier(); 147 } 148 createUserUsingShell(String name, boolean isGuest)149 private int createUserUsingShell(String name, boolean isGuest) throws Exception { 150 String retStr; 151 Log.d( 152 LOG_TAG, 153 "Creating new " + (isGuest ? "guest" : "user") + " with name '" + name + "'"); 154 if (isGuest) { 155 retStr = SystemUtil.runShellCommand(CREATE_USER_COMMAND + "--guest " + name); 156 } else { 157 retStr = SystemUtil.runShellCommand(CREATE_USER_COMMAND + name); 158 } 159 if (!retStr.contains("STATUS_SUCCESSFUL")) { 160 throw new Exception( 161 "failed to create a new user: " 162 + name 163 + ". User creation shell command output: " 164 + retStr); 165 } 166 // Extract the user ID out and return it 167 String userIdPattern = "id="; 168 int newUserId = -1; 169 if (retStr.contains(userIdPattern) && retStr.split(userIdPattern).length > 1) { 170 newUserId = Integer.parseInt(retStr.split(userIdPattern)[1].trim()); 171 } 172 return newUserId; 173 } 174 175 /** 176 * Switches to the target user at API level. Always waits until user switch complete. Besides, 177 * it waits for an additional amount of time for switched user to become idle (stable) 178 * 179 * @param id user id 180 * @param timeoutMs the time to wait (in msec) after user switch complete 181 */ switchAndWaitForStable(int id, long timeoutMs)182 public void switchAndWaitForStable(int id, long timeoutMs) throws Exception { 183 switchToUserId(id); 184 SystemClock.sleep(timeoutMs); 185 } 186 187 /** 188 * Switches to the target user at API level. Always wait until user switch complete. 189 * 190 * <p>User switch complete only means the user ready at API level. It doesn't mean the UI is 191 * completely ready for the target user. It doesn't include unlocking user data and loading car 192 * launcher page 193 * 194 * @param id Id of the user to switch to 195 */ switchToUserId(int id)196 public void switchToUserId(int id) throws Exception { 197 Log.d( 198 LOG_TAG, 199 String.format( 200 "Switching from user %d to user %d", 201 getCurrentForegroundUserInfo().id, id)); 202 final CountDownLatch latch = new CountDownLatch(1); 203 // A UserLifeCycleListener to wait for user switch event. It is equivalent to 204 // UserSwitchObserver#onUserSwitchComplete callback 205 // TODO(b/155434907): Should eventually wait for "user unlocked" event which is better 206 UserLifecycleListener userSwitchListener = 207 e -> { 208 if (e.getEventType() == CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING) { 209 latch.countDown(); 210 } 211 }; 212 mCarUserManager.addListener(Runnable::run, userSwitchListener); 213 AsyncFuture<UserSwitchResult> future = mCarUserManager.switchUser(id); 214 UserSwitchResult result = null; 215 try { 216 result = future.get(USER_SWITCH_TIMEOUT_SECOND, TimeUnit.SECONDS); 217 } catch (Exception e) { 218 throw new Exception( 219 String.format("Exception when switching to target user: %d", id), e); 220 } 221 222 if (!result.isSuccess()) { 223 throw new Exception(String.format("User switch failed: %s", result)); 224 } 225 // Wait for user switch complete event, which seems to happen later than UserSwitchResult. 226 if (!latch.await(USER_SWITCH_TIMEOUT_SECOND, TimeUnit.SECONDS)) { 227 throw new Exception( 228 String.format( 229 "Timeout while switching to user %d after %d seconds", 230 id, USER_SWITCH_TIMEOUT_SECOND)); 231 } 232 mCarUserManager.removeListener(userSwitchListener); 233 } 234 235 /** 236 * Starts a user on a display. Awaits {@link CarUserManager#USER_LIFECYCLE_EVENT_TYPE_STARTING} event. 237 * 238 * <p>User start complete only means the user ready at API level. It doesn't mean the UI is 239 * completely ready for the target user. It doesn't include unlocking user data and loading car 240 * launcher page 241 * 242 * @param id Id of the user to start 243 * @param displayId Id of the display to start the user on 244 */ startUser(int id, int displayId)245 public void startUser(int id, int displayId) throws Exception { 246 final CountDownLatch latch = new CountDownLatch(1); 247 UserLifecycleListener userStartListener = 248 e -> { 249 if (e.getEventType() == CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STARTING) { 250 latch.countDown(); 251 } 252 }; 253 mCarUserManager.addListener(Runnable::run, userStartListener); 254 UserStartRequest userStartRequest = 255 new UserStartRequest.Builder(mUserManager.getUserInfo(id).getUserHandle()) 256 .setDisplayId(displayId) 257 .build(); 258 Log.d(LOG_TAG, String.format("Starting a user with id %d on display %d", id, displayId)); 259 SyncResultCallback<UserStartResponse> userStartResponseCallback = 260 new SyncResultCallback<>(); 261 mCarUserManager.startUser(userStartRequest, Runnable::run, userStartResponseCallback); 262 UserStartResponse response = 263 userStartResponseCallback.get(USER_SWITCH_TIMEOUT_SECOND, TimeUnit.SECONDS); 264 if (!response.isSuccess()) { 265 throw new Exception( 266 String.format( 267 "Failed to start user %d on display %d: %s", id, displayId, response)); 268 } 269 if (!latch.await(USER_SWITCH_TIMEOUT_SECOND, TimeUnit.SECONDS)) { 270 throw new Exception( 271 String.format( 272 "Timeout while starting user %d after %d seconds", 273 id, USER_SWITCH_TIMEOUT_SECOND)); 274 } 275 mCarUserManager.removeListener(userStartListener); 276 } 277 278 /** 279 * Stops a user. 280 * 281 * @param id Id of the user to stop 282 */ stopUser(int id)283 public void stopUser(int id) throws Exception { 284 UserStopRequest userStopRequest = 285 new UserStopRequest.Builder(mUserManager.getUserInfo(id).getUserHandle()).build(); 286 SyncResultCallback<UserStopResponse> userStopResponseCallback = new SyncResultCallback<>(); 287 mCarUserManager.stopUser(userStopRequest, Runnable::run, userStopResponseCallback); 288 UserStopResponse response = 289 userStopResponseCallback.get(USER_SWITCH_TIMEOUT_SECOND, TimeUnit.SECONDS); 290 if (!response.isSuccess()) { 291 throw new Exception(String.format("Failed to stop user %d: %s", id, response)); 292 } 293 } 294 295 /** 296 * Removes the target user. For now it is a non-blocking call. 297 * 298 * @param userInfo info of the user to be removed 299 * @return true if removed successfully 300 */ removeUser(UserInfo userInfo)301 public boolean removeUser(UserInfo userInfo) { 302 return removeUser(userInfo.id); 303 } 304 305 /** 306 * Removes the target user. For now it is a non-blocking call. 307 * 308 * @param userId id of the user to be removed 309 * @return true if removed successfully 310 */ removeUser(int userId)311 public boolean removeUser(int userId) { 312 return mUserManager.removeUser(userId); 313 } 314 getCurrentForegroundUserInfo()315 public UserInfo getCurrentForegroundUserInfo() { 316 return mUserManager.getUserInfo(ActivityManager.getCurrentUser()); 317 } 318 319 /** 320 * Get default initial user 321 * 322 * @return user ID of initial user 323 */ getInitialUser()324 public int getInitialUser() { 325 UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 326 try { 327 String userId = device.executeShellCommand("cmd car_service get-initial-user").trim(); 328 return Integer.parseInt(userId); 329 } catch (IOException e) { 330 throw new RuntimeException("Failed to get initial user", e); 331 } 332 } 333 334 /** 335 * Tries to find an existing user with the given name 336 * 337 * @param name the name of the user 338 * @return A {@link UserInfo} if the user is found, or {@code null} if not found 339 */ 340 @Nullable getUserByName(String name)341 public UserInfo getUserByName(String name) { 342 return mUserManager.getUsers().stream() 343 .filter(user -> user.name.equals(name)) 344 .findFirst() 345 .orElse(null); 346 } 347 348 /** 349 * Returns the list of displays that are available for starting visible background users. 350 * 351 * @return The list of displays that are available for starting visible background users. 352 */ getDisplayIdsForStartingVisibleBackgroundUsers()353 public int[] getDisplayIdsForStartingVisibleBackgroundUsers() { 354 return mActivityManager.getDisplayIdsForStartingVisibleBackgroundUsers(); 355 } 356 357 /** 358 * Returns the user id for the given display id. 359 * 360 * @param displayId The display id. 361 * @return The user id for the given display id. 362 */ getUserForDisplayId(int displayId)363 public int getUserForDisplayId(int displayId) { 364 return mCarOccupantZoneManager.getUserForDisplayId(displayId); 365 } 366 } 367