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 com.android.car.settings.profiles; 17 18 import android.annotation.IntDef; 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.StringRes; 22 import android.annotation.UserIdInt; 23 import android.app.ActivityManager; 24 import android.car.Car; 25 import android.car.user.CarUserManager; 26 import android.car.user.OperationResult; 27 import android.car.user.UserCreationResult; 28 import android.car.user.UserRemovalResult; 29 import android.car.user.UserSwitchResult; 30 import android.car.util.concurrent.AsyncFuture; 31 import android.content.Context; 32 import android.content.pm.UserInfo; 33 import android.content.res.Resources; 34 import android.os.UserHandle; 35 import android.os.UserManager; 36 import android.sysprop.CarProperties; 37 import android.util.Log; 38 39 import com.android.car.settings.R; 40 import com.android.internal.annotations.VisibleForTesting; 41 42 import java.lang.annotation.Retention; 43 import java.lang.annotation.RetentionPolicy; 44 import java.util.List; 45 import java.util.concurrent.ExecutionException; 46 import java.util.concurrent.TimeUnit; 47 import java.util.concurrent.TimeoutException; 48 import java.util.function.Predicate; 49 import java.util.stream.Collectors; 50 import java.util.stream.Stream; 51 52 /** 53 * Helper class for providing basic profile logic that applies across the Settings app for Cars. 54 */ 55 public class ProfileHelper { 56 private static final String TAG = "ProfileHelper"; 57 private static final int TIMEOUT_MS = CarProperties.user_hal_timeout().orElse(5_000) + 500; 58 private static ProfileHelper sInstance; 59 60 private final UserManager mUserManager; 61 private final CarUserManager mCarUserManager; 62 private final Resources mResources; 63 private final String mDefaultAdminName; 64 private final String mDefaultGuestName; 65 66 /** 67 * Result code for when a profile was successfully marked for removal and the 68 * device switched to a different profile. 69 */ 70 public static final int REMOVE_PROFILE_RESULT_SUCCESS = 0; 71 72 /** 73 * Result code for when there was a failure removing a profile. 74 */ 75 public static final int REMOVE_PROFILE_RESULT_FAILED = 1; 76 77 /** 78 * Result code when the profile was successfully marked for removal, but the switch to a new 79 * profile failed. In this case the profile marked for removal is set as ephemeral and will be 80 * removed on the next profile switch or reboot. 81 */ 82 public static final int REMOVE_PROFILE_RESULT_SWITCH_FAILED = 2; 83 84 /** 85 * Possible return values for {@link #removeProfile(int)}, which attempts to remove a profile 86 * and switch to a new one. Note that this IntDef is distinct from {@link UserRemovalResult}, 87 * which is only a result code for the profile removal operation. 88 */ 89 @IntDef(prefix = {"REMOVE_PROFILE_RESULT"}, value = { 90 REMOVE_PROFILE_RESULT_SUCCESS, 91 REMOVE_PROFILE_RESULT_FAILED, 92 REMOVE_PROFILE_RESULT_SWITCH_FAILED, 93 }) 94 @Retention(RetentionPolicy.SOURCE) 95 public @interface RemoveProfileResult { 96 } 97 98 /** 99 * Returns an instance of ProfileHelper. 100 */ getInstance(Context context)101 public static ProfileHelper getInstance(Context context) { 102 if (sInstance == null) { 103 Context appContext = context.getApplicationContext(); 104 Resources resources = appContext.getResources(); 105 sInstance = new ProfileHelper(UserManager.get(appContext), resources, 106 resources.getString(com.android.internal.R.string.owner_name), 107 resources.getString(R.string.user_guest), 108 getCarUserManager(appContext)); 109 } 110 return sInstance; 111 } 112 113 @VisibleForTesting ProfileHelper(UserManager userManager, Resources resources, String defaultAdminName, String defaultGuestName, CarUserManager carUserManager)114 ProfileHelper(UserManager userManager, Resources resources, String defaultAdminName, 115 String defaultGuestName, CarUserManager carUserManager) { 116 mUserManager = userManager; 117 mResources = resources; 118 mDefaultAdminName = defaultAdminName; 119 mDefaultGuestName = defaultGuestName; 120 mCarUserManager = carUserManager; 121 } 122 getCarUserManager(@onNull Context context)123 private static CarUserManager getCarUserManager(@NonNull Context context) { 124 Car car = Car.createCar(context); 125 CarUserManager carUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE); 126 return carUserManager; 127 } 128 129 /** 130 * Tries to remove the profile that's passed in. System profile cannot be removed. 131 * If the profile to be removed is profile currently running the process, it switches to the 132 * guest profile first, and then removes the profile. 133 * If the profile being removed is the last admin profile, this will create a new admin profile. 134 * 135 * @param context An application context 136 * @param userInfo Profile to be removed 137 * @return {@link RemoveProfileResult} indicating the result status for profile removal and 138 * switching 139 */ 140 @RemoveProfileResult removeProfile(Context context, UserInfo userInfo)141 public int removeProfile(Context context, UserInfo userInfo) { 142 if (userInfo.id == UserHandle.USER_SYSTEM) { 143 Log.w(TAG, "User " + userInfo.id + " is system user, could not be removed."); 144 return REMOVE_PROFILE_RESULT_FAILED; 145 } 146 147 // Try to create a new admin before deleting the current one. 148 if (userInfo.isAdmin() && getAllAdminProfiles().size() <= 1) { 149 return replaceLastAdmin(userInfo); 150 } 151 152 if (!mUserManager.isAdminUser() && !isCurrentProcessUser(userInfo)) { 153 // If the caller is non-admin, they can only delete themselves. 154 Log.e(TAG, "Non-admins cannot remove other profiles."); 155 return REMOVE_PROFILE_RESULT_FAILED; 156 } 157 158 if (userInfo.id == ActivityManager.getCurrentUser()) { 159 return removeThisProfileAndSwitchToGuest(context, userInfo); 160 } 161 162 return removeProfile(userInfo.id); 163 } 164 165 /** 166 * If the ID being removed is the current foreground profile, we need to handle switching to 167 * a new or existing guest. 168 */ 169 @RemoveProfileResult removeThisProfileAndSwitchToGuest(Context context, UserInfo userInfo)170 private int removeThisProfileAndSwitchToGuest(Context context, UserInfo userInfo) { 171 if (mUserManager.getUserSwitchability() != UserManager.SWITCHABILITY_STATUS_OK) { 172 // If we can't switch to a different profile, we can't exit this one and therefore 173 // can't delete it. 174 Log.w(TAG, "Profile switching is not allowed. Current profile cannot be deleted"); 175 return REMOVE_PROFILE_RESULT_FAILED; 176 } 177 UserInfo guestUser = createNewOrFindExistingGuest(context); 178 if (guestUser == null) { 179 Log.e(TAG, "Could not create a Guest profile."); 180 return REMOVE_PROFILE_RESULT_FAILED; 181 } 182 183 // since the profile is still current, this will set it as ephemeral 184 int result = removeProfile(userInfo.id); 185 if (result != REMOVE_PROFILE_RESULT_SUCCESS) { 186 return result; 187 } 188 189 if (!switchProfile(guestUser.id)) { 190 return REMOVE_PROFILE_RESULT_SWITCH_FAILED; 191 } 192 193 return REMOVE_PROFILE_RESULT_SUCCESS; 194 } 195 196 @RemoveProfileResult removeProfile(@serIdInt int userId)197 private int removeProfile(@UserIdInt int userId) { 198 UserRemovalResult result = mCarUserManager.removeUser(userId); 199 if (Log.isLoggable(TAG, Log.INFO)) { 200 Log.i(TAG, "Remove profile result: " + result); 201 } 202 if (result.isSuccess()) { 203 return REMOVE_PROFILE_RESULT_SUCCESS; 204 } else { 205 Log.w(TAG, "Failed to remove profile " + userId + ": " + result); 206 return REMOVE_PROFILE_RESULT_FAILED; 207 } 208 } 209 switchProfile(@serIdInt int userId)210 private boolean switchProfile(@UserIdInt int userId) { 211 UserSwitchResult result = getResult("switch", mCarUserManager.switchUser(userId)); 212 return result != null && result.isSuccess(); 213 } 214 215 /** 216 * Returns the {@link StringRes} that corresponds to a {@link RemoveProfileResult} result code. 217 */ 218 @StringRes getErrorMessageForProfileResult(@emoveProfileResult int result)219 public int getErrorMessageForProfileResult(@RemoveProfileResult int result) { 220 if (result == REMOVE_PROFILE_RESULT_SWITCH_FAILED) { 221 return R.string.delete_user_error_set_ephemeral_title; 222 } 223 224 return R.string.delete_user_error_title; 225 } 226 227 /** 228 * Gets the result of an async operation. 229 * 230 * @param operation name of the operation, to be logged in case of error 231 * @param future future holding the operation result. 232 * @return result of the operation or {@code null} if it failed or timed out. 233 */ 234 @Nullable getResult(String operation, AsyncFuture<T> future)235 private static <T extends OperationResult> T getResult(String operation, 236 AsyncFuture<T> future) { 237 T result = null; 238 try { 239 result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); 240 } catch (InterruptedException e) { 241 Thread.currentThread().interrupt(); 242 Log.w(TAG, "Interrupted waiting to " + operation + " profile", e); 243 return null; 244 } catch (ExecutionException | TimeoutException e) { 245 Log.w(TAG, "Exception waiting to " + operation + " profile", e); 246 return null; 247 } 248 if (result == null) { 249 Log.w(TAG, "Time out (" + TIMEOUT_MS + " ms) trying to " + operation + " profile"); 250 return null; 251 } 252 if (!result.isSuccess()) { 253 Log.w(TAG, "Failed to " + operation + " profile: " + result); 254 return null; 255 } 256 return result; 257 } 258 259 @RemoveProfileResult replaceLastAdmin(UserInfo userInfo)260 private int replaceLastAdmin(UserInfo userInfo) { 261 if (Log.isLoggable(TAG, Log.INFO)) { 262 Log.i(TAG, "Profile " + userInfo.id 263 + " is the last admin profile on device. Creating a new admin."); 264 } 265 266 UserInfo newAdmin = createNewAdminProfile(mDefaultAdminName); 267 if (newAdmin == null) { 268 Log.w(TAG, "Couldn't create another admin, cannot delete current profile."); 269 return REMOVE_PROFILE_RESULT_FAILED; 270 } 271 272 int removeUserResult = removeProfile(userInfo.id); 273 if (removeUserResult != REMOVE_PROFILE_RESULT_SUCCESS) { 274 return removeUserResult; 275 } 276 277 if (switchProfile(newAdmin.id)) { 278 return REMOVE_PROFILE_RESULT_SUCCESS; 279 } else { 280 return REMOVE_PROFILE_RESULT_SWITCH_FAILED; 281 } 282 } 283 284 /** 285 * Creates a new profile on the system, the created profile would be granted admin role. 286 * Only admins can create other admins. 287 * 288 * @param userName Name to give to the newly created profile. 289 * @return Newly created admin profile, null if failed to create a profile. 290 */ 291 @Nullable createNewAdminProfile(String userName)292 private UserInfo createNewAdminProfile(String userName) { 293 if (!(mUserManager.isAdminUser() || mUserManager.isSystemUser())) { 294 // Only Admins or System profile can create other privileged profiles. 295 Log.e(TAG, "Only admin profiles and system profile can create other admins."); 296 return null; 297 } 298 UserCreationResult result = getResult("create admin", 299 mCarUserManager.createUser(userName, UserInfo.FLAG_ADMIN)); 300 if (result == null) return null; 301 UserInfo user = result.getUser(); 302 303 new ProfileIconProvider().assignDefaultIcon(mUserManager, mResources, user); 304 return user; 305 } 306 307 /** 308 * Creates and returns a new guest profile or returns the existing one. 309 * Returns null if it fails to create a new guest. 310 * 311 * @param context an application context 312 * @return The UserInfo representing the Guest, or null if it failed 313 */ 314 @Nullable createNewOrFindExistingGuest(Context context)315 public UserInfo createNewOrFindExistingGuest(Context context) { 316 // createGuest() will return null if a guest already exists. 317 UserCreationResult result = getResult("create guest", 318 mCarUserManager.createGuest(mDefaultGuestName)); 319 UserInfo newGuest = result == null ? null : result.getUser(); 320 321 if (newGuest != null) { 322 new ProfileIconProvider().assignDefaultIcon(mUserManager, mResources, newGuest); 323 return newGuest; 324 } 325 326 return mUserManager.findCurrentGuestUser(); 327 } 328 329 /** 330 * Checks if the current process profile can modify accounts. Demo and Guest profiles cannot 331 * modify accounts even if the DISALLOW_MODIFY_ACCOUNTS restriction is not applied. 332 */ canCurrentProcessModifyAccounts()333 public boolean canCurrentProcessModifyAccounts() { 334 return !mUserManager.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS) 335 && !mUserManager.isDemoUser() 336 && !mUserManager.isGuestUser(); 337 } 338 339 /** 340 * Returns a list of {@code UserInfo} representing all profiles that can be brought to the 341 * foreground. 342 */ getAllProfiles()343 public List<UserInfo> getAllProfiles() { 344 return getAllLivingProfiles(/* filter= */ null); 345 } 346 347 /** 348 * Returns a list of {@code UserInfo} representing all profiles that can be swapped with the 349 * current profile into the foreground. 350 */ getAllSwitchableProfiles()351 public List<UserInfo> getAllSwitchableProfiles() { 352 final int foregroundUserId = ActivityManager.getCurrentUser(); 353 return getAllLivingProfiles(userInfo -> userInfo.id != foregroundUserId); 354 } 355 356 /** 357 * Returns a list of {@code UserInfo} representing all profiles that are non-ephemeral and are 358 * valid to have in the foreground. 359 */ getAllPersistentProfiles()360 public List<UserInfo> getAllPersistentProfiles() { 361 return getAllLivingProfiles(userInfo -> !userInfo.isEphemeral()); 362 } 363 364 /** 365 * Returns a list of {@code UserInfo} representing all admin profiles and are 366 * valid to have in the foreground. 367 */ getAllAdminProfiles()368 public List<UserInfo> getAllAdminProfiles() { 369 return getAllLivingProfiles(UserInfo::isAdmin); 370 } 371 372 /** 373 * Gets all profiles that are not dying. This method will handle 374 * {@link UserManager#isHeadlessSystemUserMode} and ensure the system profile is not 375 * part of the return list when the flag is on. 376 * @param filter Optional filter to apply to the list of profiles. Pass null to skip. 377 * @return An optionally filtered list containing all living profiles 378 */ getAllLivingProfiles(@ullable Predicate<? super UserInfo> filter)379 public List<UserInfo> getAllLivingProfiles(@Nullable Predicate<? super UserInfo> filter) { 380 Stream<UserInfo> filteredListStream = mUserManager.getAliveUsers().stream(); 381 382 if (filter != null) { 383 filteredListStream = filteredListStream.filter(filter); 384 } 385 386 if (UserManager.isHeadlessSystemUserMode()) { 387 filteredListStream = 388 filteredListStream.filter(userInfo -> userInfo.id != UserHandle.USER_SYSTEM); 389 } 390 return filteredListStream.collect(Collectors.toList()); 391 } 392 393 /** 394 * Checks whether passed in user is the user that's running the current process. 395 * 396 * @param userInfo User to check. 397 * @return {@code true} if user running the process, {@code false} otherwise. 398 */ isCurrentProcessUser(UserInfo userInfo)399 public boolean isCurrentProcessUser(UserInfo userInfo) { 400 return UserHandle.myUserId() == userInfo.id; 401 } 402 403 /** 404 * Gets UserInfo for the user running the caller process. 405 * 406 * <p>Differentiation between foreground user and current process user is relevant for 407 * multi-user deployments. 408 * 409 * <p>Some multi-user aware components (like SystemUI) needs to run a singleton component 410 * in system user. Current process user is always the same for that component, even when 411 * the foreground user changes. 412 * 413 * @return {@link UserInfo} for the user running the current process. 414 */ getCurrentProcessUserInfo()415 public UserInfo getCurrentProcessUserInfo() { 416 return mUserManager.getUserInfo(UserHandle.myUserId()); 417 } 418 419 /** 420 * Maximum number of profiles allowed on the device. This includes real profiles, managed 421 * profiles and restricted profiles, but excludes guests. 422 * 423 * <p> It excludes system profile in headless system profile model. 424 * 425 * @return Maximum number of profiles that can be present on the device. 426 */ getMaxSupportedProfiles()427 private int getMaxSupportedProfiles() { 428 int maxSupportedUsers = UserManager.getMaxSupportedUsers(); 429 if (UserManager.isHeadlessSystemUserMode()) { 430 maxSupportedUsers -= 1; 431 } 432 return maxSupportedUsers; 433 } 434 getManagedProfilesCount()435 private int getManagedProfilesCount() { 436 List<UserInfo> users = getAllProfiles(); 437 438 // Count all users that are managed profiles of another user. 439 int managedProfilesCount = 0; 440 for (UserInfo user : users) { 441 if (user.isManagedProfile()) { 442 managedProfilesCount++; 443 } 444 } 445 return managedProfilesCount; 446 } 447 448 /** 449 * Get the maximum number of real (non-guest, non-managed profile) profiles that can be created 450 * on the device. This is a dynamic value and it decreases with the increase of the number of 451 * managed profiles on the device. 452 * 453 * <p> It excludes system profile in headless system profile model. 454 * 455 * @return Maximum number of real profiles that can be created. 456 */ getMaxSupportedRealProfiles()457 public int getMaxSupportedRealProfiles() { 458 return getMaxSupportedProfiles() - getManagedProfilesCount(); 459 } 460 } 461