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