• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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