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