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