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