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