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