• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.systemui.car.qc;
17 
18 import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
19 import static android.provider.Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS;
20 import static android.view.WindowInsets.Type.statusBars;
21 
22 import static com.android.car.ui.utils.CarUiUtils.drawableToBitmap;
23 
24 import android.annotation.Nullable;
25 import android.annotation.UserIdInt;
26 import android.app.ActivityManager;
27 import android.app.AlertDialog;
28 import android.app.admin.DevicePolicyManager;
29 import android.car.Car;
30 import android.car.user.CarUserManager;
31 import android.car.user.UserCreationResult;
32 import android.car.user.UserSwitchResult;
33 import android.car.util.concurrent.AsyncFuture;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.pm.UserInfo;
37 import android.graphics.drawable.Drawable;
38 import android.graphics.drawable.Icon;
39 import android.os.AsyncTask;
40 import android.os.UserHandle;
41 import android.os.UserManager;
42 import android.sysprop.CarProperties;
43 import android.util.Log;
44 import android.view.Window;
45 import android.view.WindowManager;
46 
47 import androidx.annotation.NonNull;
48 import androidx.annotation.VisibleForTesting;
49 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
50 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
51 
52 import com.android.car.internal.user.UserHelper;
53 import com.android.car.qc.QCItem;
54 import com.android.car.qc.QCList;
55 import com.android.car.qc.QCRow;
56 import com.android.car.qc.provider.BaseLocalQCProvider;
57 import com.android.internal.util.UserIcons;
58 import com.android.settingslib.utils.StringUtil;
59 import com.android.systemui.R;
60 import com.android.systemui.car.userswitcher.UserIconProvider;
61 
62 import java.util.List;
63 import java.util.concurrent.TimeUnit;
64 import java.util.stream.Collectors;
65 
66 /**
67  * Local provider for the profile switcher panel.
68  */
69 public class ProfileSwitcher extends BaseLocalQCProvider {
70     private static final String TAG = ProfileSwitcher.class.getSimpleName();
71     private static final int TIMEOUT_MS = CarProperties.user_hal_timeout().orElse(5_000) + 500;
72 
73     private final UserManager mUserManager;
74     private final DevicePolicyManager mDevicePolicyManager;
75     private final UserIconProvider mUserIconProvider;
76     private final Car mCar;
77     private final CarUserManager mCarUserManager;
78     private boolean mPendingUserAdd;
79 
ProfileSwitcher(Context context)80     public ProfileSwitcher(Context context) {
81         super(context);
82         mUserManager = context.getSystemService(UserManager.class);
83         mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
84         mUserIconProvider = new UserIconProvider();
85         mCar = Car.createCar(context);
86         mCarUserManager = (CarUserManager) mCar.getCarManager(Car.CAR_USER_SERVICE);
87     }
88 
89     @VisibleForTesting
ProfileSwitcher(Context context, UserManager userManager, DevicePolicyManager devicePolicyManager, CarUserManager carUserManager)90     ProfileSwitcher(Context context, UserManager userManager,
91             DevicePolicyManager devicePolicyManager, CarUserManager carUserManager) {
92         super(context);
93         mUserManager = userManager;
94         mDevicePolicyManager = devicePolicyManager;
95         mUserIconProvider = new UserIconProvider();
96         mCar = null;
97         mCarUserManager = carUserManager;
98     }
99 
100     @Override
getQCItem()101     public QCItem getQCItem() {
102         QCList.Builder listBuilder = new QCList.Builder();
103 
104         if (mDevicePolicyManager.isDeviceManaged()
105                 || mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()) {
106             listBuilder.addRow(createOrganizationOwnedDeviceRow());
107         }
108 
109         boolean isLogoutEnabled = mDevicePolicyManager.isLogoutEnabled()
110                 && mDevicePolicyManager.getLogoutUser() != null;
111 
112         int fgUserId = ActivityManager.getCurrentUser();
113         UserHandle fgUserHandle = UserHandle.of(fgUserId);
114         // If the foreground user CANNOT switch to other users, only display the foreground user.
115         if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) {
116             UserInfo currentUser = mUserManager.getUserInfo(ActivityManager.getCurrentUser());
117             listBuilder.addRow(createUserProfileRow(currentUser));
118             if (isLogoutEnabled) {
119                 listBuilder.addRow(createLogOutRow());
120             }
121             return listBuilder.build();
122         }
123 
124         List<UserInfo> profiles = getProfileList();
125         for (UserInfo profile : profiles) {
126             listBuilder.addRow(createUserProfileRow(profile));
127         }
128         listBuilder.addRow(createGuestProfileRow());
129         if (!hasAddUserRestriction(fgUserHandle)) {
130             listBuilder.addRow(createAddProfileRow());
131         }
132 
133         if (isLogoutEnabled) {
134             listBuilder.addRow(createLogOutRow());
135         }
136         return listBuilder.build();
137     }
138 
139     @Override
onDestroy()140     public void onDestroy() {
141         super.onDestroy();
142         if (mCar != null) {
143             mCar.disconnect();
144         }
145     }
146 
getProfileList()147     private List<UserInfo> getProfileList() {
148         return mUserManager.getAliveUsers()
149                 .stream()
150                 .filter(userInfo -> userInfo.supportsSwitchToByUser() && !userInfo.isGuest())
151                 .sorted((u1, u2) -> Long.signum(u1.creationTime - u2.creationTime))
152                 .collect(Collectors.toList());
153     }
154 
createOrganizationOwnedDeviceRow()155     private QCRow createOrganizationOwnedDeviceRow() {
156         Icon icon = Icon.createWithBitmap(
157                 drawableToBitmap(mContext.getDrawable(R.drawable.car_ic_managed_device)));
158         QCRow row = new QCRow.Builder()
159                 .setIcon(icon)
160                 .setSubtitle(mContext.getString(R.string.do_disclosure_generic))
161                 .build();
162         row.setActionHandler(new QCItem.ActionHandler() {
163             @Override
164             public void onAction(@NonNull QCItem item, @NonNull Context context,
165                     @NonNull Intent intent) {
166                 mContext.startActivityAsUser(new Intent(ACTION_ENTERPRISE_PRIVACY_SETTINGS),
167                         UserHandle.CURRENT);
168             }
169 
170             @Override
171             public boolean isActivity() {
172                 return true;
173             }
174         });
175         return row;
176     }
177 
createUserProfileRow(UserInfo userInfo)178     private QCRow createUserProfileRow(UserInfo userInfo) {
179         QCItem.ActionHandler actionHandler = (item, context, intent) -> {
180             if (mPendingUserAdd) {
181                 return;
182             }
183             switchUser(userInfo.id);
184         };
185 
186         return createProfileRow(userInfo.name,
187                 mUserIconProvider.getDrawableWithBadge(mContext, userInfo), actionHandler);
188     }
189 
createGuestProfileRow()190     private QCRow createGuestProfileRow() {
191         QCItem.ActionHandler actionHandler = (item, context, intent) -> {
192             if (mPendingUserAdd) {
193                 return;
194             }
195             UserInfo guest = createNewOrFindExistingGuest(mContext);
196             if (guest != null) {
197                 switchUser(guest.id);
198             }
199         };
200 
201         return createProfileRow(mContext.getString(com.android.internal.R.string.guest_name),
202                 mUserIconProvider.getRoundedGuestDefaultIcon(mContext.getResources()),
203                 actionHandler);
204     }
205 
createAddProfileRow()206     private QCRow createAddProfileRow() {
207         QCItem.ActionHandler actionHandler = (item, context, intent) -> {
208             if (mPendingUserAdd) {
209                 return;
210             }
211             if (!mUserManager.canAddMoreUsers()) {
212                 showMaxUserLimitReachedDialog();
213             } else {
214                 showConfirmAddUserDialog();
215             }
216         };
217 
218         return createProfileRow(mContext.getString(R.string.car_add_user),
219                 mUserIconProvider.getDrawableWithBadge(mContext, getCircularAddUserIcon()),
220                 actionHandler);
221     }
222 
createLogOutRow()223     private QCRow createLogOutRow() {
224         QCRow row = new QCRow.Builder()
225                 .setIcon(Icon.createWithResource(mContext, R.drawable.car_ic_logout))
226                 .setTitle(mContext.getString(R.string.end_session))
227                 .build();
228         row.setActionHandler((item, context, intent) -> logoutUser());
229         return row;
230     }
231 
createProfileRow(String title, Drawable iconDrawable, QCItem.ActionHandler actionHandler)232     private QCRow createProfileRow(String title, Drawable iconDrawable,
233             QCItem.ActionHandler actionHandler) {
234         Icon icon = Icon.createWithBitmap(drawableToBitmap(iconDrawable));
235         QCRow row = new QCRow.Builder()
236                 .setIcon(icon)
237                 .setIconTintable(false)
238                 .setTitle(title)
239                 .build();
240         row.setActionHandler(actionHandler);
241         return row;
242     }
243 
switchUser(@serIdInt int userId)244     private void switchUser(@UserIdInt int userId) {
245         mContext.sendBroadcastAsUser(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
246                 UserHandle.CURRENT);
247         AsyncFuture<UserSwitchResult> userSwitchResultFuture =
248                 mCarUserManager.switchUser(userId);
249         UserSwitchResult userSwitchResult;
250         try {
251             userSwitchResult = userSwitchResultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
252         } catch (Exception e) {
253             Log.w(TAG, "Could not switch user.", e);
254             return;
255         }
256         if (userSwitchResult == null) {
257             Log.w(TAG, "Timed out while switching user: " + TIMEOUT_MS + "ms");
258         } else if (!userSwitchResult.isSuccess()) {
259             Log.w(TAG, "Could not switch user: " + userSwitchResult);
260         }
261     }
262 
logoutUser()263     private void logoutUser() {
264         mContext.sendBroadcastAsUser(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
265                 UserHandle.CURRENT);
266         AsyncFuture<UserSwitchResult> userSwitchResultFuture = mCarUserManager.logoutUser();
267         UserSwitchResult userSwitchResult;
268         try {
269             userSwitchResult = userSwitchResultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
270         } catch (Exception e) {
271             Log.w(TAG, "Could not log out user.", e);
272             return;
273         }
274         if (userSwitchResult == null) {
275             Log.w(TAG, "Timed out while logging out user: " + TIMEOUT_MS + "ms");
276         } else if (!userSwitchResult.isSuccess()) {
277             Log.w(TAG, "Could not log out user: " + userSwitchResult);
278         }
279     }
280 
281     /**
282      * Finds the existing Guest user, or creates one if it doesn't exist.
283      *
284      * @param context App context
285      * @return UserInfo representing the Guest user
286      */
287     @Nullable
createNewOrFindExistingGuest(Context context)288     private UserInfo createNewOrFindExistingGuest(Context context) {
289         AsyncFuture<UserCreationResult> future = mCarUserManager.createGuest(
290                 context.getString(com.android.internal.R.string.guest_name));
291         // CreateGuest will return null if a guest already exists.
292         UserInfo newGuest = getUserInfo(future);
293         if (newGuest != null) {
294             new UserIconProvider().assignDefaultIcon(
295                     mUserManager, context.getResources(), newGuest);
296             return newGuest;
297         }
298         return mUserManager.findCurrentGuestUser();
299     }
300 
301     @Nullable
getUserInfo(AsyncFuture<UserCreationResult> future)302     private UserInfo getUserInfo(AsyncFuture<UserCreationResult> future) {
303         UserCreationResult userCreationResult;
304         try {
305             userCreationResult = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
306         } catch (Exception e) {
307             Log.w(TAG, "Could not create user.", e);
308             return null;
309         }
310         if (userCreationResult == null) {
311             Log.w(TAG, "Timed out while creating user: " + TIMEOUT_MS + "ms");
312             return null;
313         }
314         if (!userCreationResult.isSuccess() || userCreationResult.getUser() == null) {
315             Log.w(TAG, "Could not create user: " + userCreationResult);
316             return null;
317         }
318         return mUserManager.getUserInfo(userCreationResult.getUser().getIdentifier());
319     }
320 
getCircularAddUserIcon()321     private RoundedBitmapDrawable getCircularAddUserIcon() {
322         RoundedBitmapDrawable circleIcon = RoundedBitmapDrawableFactory.create(
323                 mContext.getResources(),
324                 UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.car_add_circle_round)));
325         circleIcon.setCircular(true);
326         return circleIcon;
327     }
328 
hasAddUserRestriction(UserHandle userHandle)329     private boolean hasAddUserRestriction(UserHandle userHandle) {
330         return mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_ADD_USER, userHandle);
331     }
332 
getMaxSupportedRealUsers()333     private int getMaxSupportedRealUsers() {
334         int maxSupportedUsers = UserManager.getMaxSupportedUsers();
335         if (UserManager.isHeadlessSystemUserMode()) {
336             maxSupportedUsers -= 1;
337         }
338         List<UserInfo> users = mUserManager.getAliveUsers();
339         // Count all users that are managed profiles of another user.
340         int managedProfilesCount = 0;
341         for (UserInfo user : users) {
342             if (user.isManagedProfile()) {
343                 managedProfilesCount++;
344             }
345         }
346         return maxSupportedUsers - managedProfilesCount;
347     }
348 
showMaxUserLimitReachedDialog()349     private void showMaxUserLimitReachedDialog() {
350         AlertDialog maxUsersDialog = new AlertDialog.Builder(mContext,
351                 com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
352                 .setTitle(R.string.profile_limit_reached_title)
353                 .setMessage(StringUtil.getIcuPluralsString(mContext, getMaxSupportedRealUsers(),
354                                 R.string.profile_limit_reached_message))
355                 .setPositiveButton(android.R.string.ok, null)
356                 .create();
357         // Sets window flags for the SysUI dialog
358         applyCarSysUIDialogFlags(maxUsersDialog);
359         maxUsersDialog.show();
360     }
361 
showConfirmAddUserDialog()362     private void showConfirmAddUserDialog() {
363         String message = mContext.getString(R.string.user_add_user_message_setup)
364                 .concat(System.getProperty("line.separator"))
365                 .concat(System.getProperty("line.separator"))
366                 .concat(mContext.getString(R.string.user_add_user_message_update));
367         AlertDialog addUserDialog = new AlertDialog.Builder(mContext,
368                 com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
369                 .setTitle(R.string.user_add_profile_title)
370                 .setMessage(message)
371                 .setNegativeButton(android.R.string.cancel, null)
372                 .setPositiveButton(android.R.string.ok,
373                         (dialog, which) -> new AddNewUserTask().execute(
374                                 mContext.getString(R.string.car_new_user)))
375                 .create();
376         // Sets window flags for the SysUI dialog
377         applyCarSysUIDialogFlags(addUserDialog);
378         addUserDialog.show();
379     }
380 
applyCarSysUIDialogFlags(AlertDialog dialog)381     private void applyCarSysUIDialogFlags(AlertDialog dialog) {
382         Window window = dialog.getWindow();
383         window.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
384         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
385                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
386         window.getAttributes().setFitInsetsTypes(
387                 window.getAttributes().getFitInsetsTypes() & ~statusBars());
388     }
389 
390     private class AddNewUserTask extends AsyncTask<String, Void, UserInfo> {
391         @Override
doInBackground(String... userNames)392         protected UserInfo doInBackground(String... userNames) {
393             AsyncFuture<UserCreationResult> future = mCarUserManager.createUser(userNames[0],
394                     /* flags= */ 0);
395             try {
396                 UserInfo user = getUserInfo(future);
397                 if (user != null) {
398                     UserHelper.setDefaultNonAdminRestrictions(mContext, user.getUserHandle(),
399                             /* enable= */ true);
400                     UserHelper.assignDefaultIcon(mContext, user.getUserHandle());
401                     return user;
402                 } else {
403                     Log.e(TAG, "Failed to create user in the background");
404                     return user;
405                 }
406             } catch (Exception e) {
407                 if (e instanceof InterruptedException) {
408                     Thread.currentThread().interrupt();
409                 }
410                 Log.e(TAG, "Error creating new user: ", e);
411             }
412             return null;
413         }
414 
415         @Override
onPreExecute()416         protected void onPreExecute() {
417             mPendingUserAdd = true;
418         }
419 
420         @Override
onPostExecute(UserInfo user)421         protected void onPostExecute(UserInfo user) {
422             mPendingUserAdd = false;
423             if (user != null) {
424                 switchUser(user.id);
425             }
426         }
427     }
428 }
429