• 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.AlertDialog;
27 import android.app.admin.DevicePolicyManager;
28 import android.car.Car;
29 import android.car.SyncResultCallback;
30 import android.car.user.CarUserManager;
31 import android.car.user.UserCreationResult;
32 import android.car.user.UserStartRequest;
33 import android.car.user.UserStopRequest;
34 import android.car.user.UserStopResponse;
35 import android.car.user.UserSwitchRequest;
36 import android.car.user.UserSwitchResult;
37 import android.car.util.concurrent.AsyncFuture;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.content.pm.UserInfo;
41 import android.graphics.drawable.Drawable;
42 import android.graphics.drawable.Icon;
43 import android.os.AsyncTask;
44 import android.os.Handler;
45 import android.os.UserHandle;
46 import android.os.UserManager;
47 import android.sysprop.CarProperties;
48 import android.util.Log;
49 import android.view.Window;
50 import android.view.WindowManager;
51 import android.widget.Toast;
52 
53 import androidx.annotation.NonNull;
54 import androidx.annotation.VisibleForTesting;
55 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
56 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
57 
58 import com.android.car.internal.user.UserHelper;
59 import com.android.car.qc.QCItem;
60 import com.android.car.qc.QCList;
61 import com.android.car.qc.QCRow;
62 import com.android.car.qc.provider.BaseLocalQCProvider;
63 import com.android.internal.util.UserIcons;
64 import com.android.settingslib.utils.StringUtil;
65 import com.android.systemui.R;
66 import com.android.systemui.car.CarServiceProvider;
67 import com.android.systemui.car.users.CarSystemUIUserUtil;
68 import com.android.systemui.car.userswitcher.UserIconProvider;
69 import com.android.systemui.dagger.qualifiers.Background;
70 import com.android.systemui.settings.UserTracker;
71 
72 import java.util.List;
73 import java.util.concurrent.TimeUnit;
74 import java.util.stream.Collectors;
75 
76 import javax.inject.Inject;
77 
78 /**
79  * Local provider for the profile switcher panel.
80  */
81 public class ProfileSwitcher extends BaseLocalQCProvider {
82     private static final String TAG = ProfileSwitcher.class.getSimpleName();
83     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
84     private static final int TIMEOUT_MS = CarProperties.user_hal_timeout().orElse(5_000) + 500;
85 
86     protected final UserTracker mUserTracker;
87     protected final UserIconProvider mUserIconProvider;
88     private final UserManager mUserManager;
89     private final DevicePolicyManager mDevicePolicyManager;
90     public final Handler mHandler;
91     @Nullable
92     private CarUserManager mCarUserManager;
93     protected boolean mPendingUserAdd;
94 
95     @Inject
ProfileSwitcher(Context context, UserTracker userTracker, CarServiceProvider carServiceProvider, @Background Handler handler)96     public ProfileSwitcher(Context context, UserTracker userTracker,
97             CarServiceProvider carServiceProvider, @Background Handler handler) {
98         super(context);
99         mUserTracker = userTracker;
100         mUserManager = context.getSystemService(UserManager.class);
101         mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
102         mUserIconProvider = new UserIconProvider();
103         mHandler = handler;
104         carServiceProvider.addListener(this::onCarConnected);
105     }
106 
107     @VisibleForTesting
ProfileSwitcher(Context context, UserTracker userTracker, UserManager userManager, DevicePolicyManager devicePolicyManager, CarUserManager carUserManager, UserIconProvider userIconProvider, Handler handler)108     ProfileSwitcher(Context context, UserTracker userTracker, UserManager userManager,
109             DevicePolicyManager devicePolicyManager, CarUserManager carUserManager,
110             UserIconProvider userIconProvider, Handler handler) {
111         super(context);
112         mUserTracker = userTracker;
113         mUserManager = userManager;
114         mDevicePolicyManager = devicePolicyManager;
115         mUserIconProvider = userIconProvider;
116         mCarUserManager = carUserManager;
117         mHandler = handler;
118     }
119 
120     @Override
getQCItem()121     public QCItem getQCItem() {
122         if (mCarUserManager == null) {
123             return null;
124         }
125         QCList.Builder listBuilder = new QCList.Builder();
126 
127         if (mDevicePolicyManager.isDeviceManaged()
128                 || mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()) {
129             listBuilder.addRow(createOrganizationOwnedDeviceRow());
130         }
131 
132         boolean isLogoutEnabled = mDevicePolicyManager.isLogoutEnabled()
133                 && mDevicePolicyManager.getLogoutUser() != null;
134 
135         int fgUserId = mUserTracker.getUserId();
136         UserHandle fgUserHandle = UserHandle.of(fgUserId);
137         // If the foreground user CANNOT switch to other users, only display the foreground user.
138         if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) {
139             UserInfo currentUser = mUserManager.getUserInfo(mUserTracker.getUserId());
140             listBuilder.addRow(createUserProfileRow(currentUser));
141             if (isLogoutEnabled) {
142                 listBuilder.addRow(createLogOutRow());
143             }
144             return listBuilder.build();
145         }
146 
147         List<UserInfo> profiles = getProfileList();
148         for (UserInfo profile : profiles) {
149             listBuilder.addRow(createUserProfileRow(profile));
150         }
151         listBuilder.addRow(createGuestProfileRow());
152         if (!hasAddUserRestriction(fgUserHandle)) {
153             listBuilder.addRow(createAddProfileRow());
154         }
155 
156         if (isLogoutEnabled) {
157             listBuilder.addRow(createLogOutRow());
158         }
159         return listBuilder.build();
160     }
161 
getProfileList()162     private List<UserInfo> getProfileList() {
163         return mUserManager.getAliveUsers()
164                 .stream()
165                 .filter(userInfo -> userInfo.supportsSwitchTo() && userInfo.isFull()
166                         && !userInfo.isGuest())
167                 .sorted((u1, u2) -> Long.signum(u1.creationTime - u2.creationTime))
168                 .collect(Collectors.toList());
169     }
170 
createOrganizationOwnedDeviceRow()171     private QCRow createOrganizationOwnedDeviceRow() {
172         Icon icon = Icon.createWithBitmap(
173                 drawableToBitmap(mContext.getDrawable(R.drawable.car_ic_managed_device)));
174         QCRow row = new QCRow.Builder()
175                 .setIcon(icon)
176                 .setSubtitle(mContext.getString(R.string.do_disclosure_generic))
177                 .build();
178         row.setActionHandler(new QCItem.ActionHandler() {
179             @Override
180             public void onAction(@NonNull QCItem item, @NonNull Context context,
181                     @NonNull Intent intent) {
182                 mContext.startActivityAsUser(new Intent(ACTION_ENTERPRISE_PRIVACY_SETTINGS),
183                         mUserTracker.getUserHandle());
184             }
185 
186             @Override
187             public boolean isActivity() {
188                 return true;
189             }
190         });
191         return row;
192     }
193 
createUserProfileRow(UserInfo userInfo)194     protected QCRow createUserProfileRow(UserInfo userInfo) {
195         QCItem.ActionHandler actionHandler = (item, context, intent) -> {
196             if (mPendingUserAdd) {
197                 return;
198             }
199             switchUser(userInfo.id);
200         };
201 
202         return createProfileRow(userInfo.name,
203                 mUserIconProvider.getDrawableWithBadge(mContext, userInfo), actionHandler);
204     }
205 
createGuestProfileRow()206     protected QCRow createGuestProfileRow() {
207         QCItem.ActionHandler actionHandler = (item, context, intent) -> {
208             if (mPendingUserAdd) {
209                 return;
210             }
211             UserInfo guest = createNewOrFindExistingGuest(mContext);
212             if (guest != null) {
213                 switchUser(guest.id);
214             }
215         };
216 
217         return createProfileRow(mContext.getString(com.android.internal.R.string.guest_name),
218                 mUserIconProvider.getRoundedGuestDefaultIcon(mContext),
219                 actionHandler);
220     }
221 
createAddProfileRow()222     private QCRow createAddProfileRow() {
223         QCItem.ActionHandler actionHandler = (item, context, intent) -> {
224             if (mPendingUserAdd) {
225                 return;
226             }
227             if (!mUserManager.canAddMoreUsers()) {
228                 showMaxUserLimitReachedDialog();
229             } else {
230                 showConfirmAddUserDialog();
231             }
232         };
233 
234         return createProfileRow(mContext.getString(R.string.car_add_user),
235                 mUserIconProvider.getDrawableWithBadge(mContext, getCircularAddUserIcon()),
236                 actionHandler);
237     }
238 
createLogOutRow()239     private QCRow createLogOutRow() {
240         QCRow row = new QCRow.Builder()
241                 .setIcon(Icon.createWithResource(mContext, R.drawable.car_ic_logout))
242                 .setTitle(mContext.getString(R.string.end_session))
243                 .build();
244         row.setActionHandler((item, context, intent) -> logoutUser());
245         return row;
246     }
247 
createProfileRow(String title, Drawable iconDrawable, QCItem.ActionHandler actionHandler)248     private QCRow createProfileRow(String title, Drawable iconDrawable,
249             QCItem.ActionHandler actionHandler) {
250         Icon icon = Icon.createWithBitmap(drawableToBitmap(iconDrawable));
251         QCRow row = new QCRow.Builder()
252                 .setIcon(icon)
253                 .setIconTintable(false)
254                 .setTitle(title)
255                 .build();
256         row.setActionHandler(actionHandler);
257         return row;
258     }
259 
switchUser(@serIdInt int userId)260     protected void switchUser(@UserIdInt int userId) {
261         mContext.sendBroadcastAsUser(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
262                 mUserTracker.getUserHandle());
263         if (mUserTracker.getUserId() == userId) {
264             return;
265         }
266         if (mUserManager.isVisibleBackgroundUsersSupported()) {
267             if (mUserManager.getVisibleUsers().stream().anyMatch(
268                     userHandle -> userHandle.getIdentifier() == userId)) {
269                 // TODO_MD - finalize behavior for non-switchable users
270                 Toast.makeText(mContext,
271                         "Cannot switch to user already running on another display.",
272                         Toast.LENGTH_LONG).show();
273                 return;
274             }
275             if (CarSystemUIUserUtil.isSecondaryMUMDSystemUI()) {
276                 switchSecondaryUser(userId);
277                 return;
278             }
279         }
280         switchForegroundUser(userId);
281     }
282 
switchForegroundUser(@serIdInt int userId)283     private void switchForegroundUser(@UserIdInt int userId) {
284         // Switch user in the background thread to avoid ANR in UI thread.
285         mHandler.post(() -> {
286             UserSwitchResult userSwitchResult = null;
287             try {
288                 SyncResultCallback<UserSwitchResult> userSwitchCallback =
289                         new SyncResultCallback<>();
290                 mCarUserManager.switchUser(
291                         new UserSwitchRequest.Builder(UserHandle.of(userId)).build(),
292                         Runnable::run, userSwitchCallback);
293                 userSwitchResult = userSwitchCallback.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
294             } catch (Exception e) {
295                 Log.w(TAG, "Exception while switching to the user " + userId, e);
296             }
297             if (userSwitchResult == null || !userSwitchResult.isSuccess()) {
298                 Log.w(TAG, "Could not switch user: " + userSwitchResult);
299             }
300         });
301     }
302 
switchSecondaryUser(@serIdInt int userId)303     private void switchSecondaryUser(@UserIdInt int userId) {
304         // Switch user in the background thread to avoid ANR in UI thread.
305         mHandler.post(() -> {
306             try {
307                 SyncResultCallback<UserStopResponse> userStopCallback = new SyncResultCallback<>();
308                 mCarUserManager.stopUser(new UserStopRequest.Builder(
309                                 mUserTracker.getUserHandle()).setForce().build(),
310                         Runnable::run, userStopCallback);
311                 UserStopResponse userStopResponse =
312                         userStopCallback.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
313                 if (!userStopResponse.isSuccess()) {
314                     Log.w(TAG, "Could not stop user " + mUserTracker.getUserId() + ". Response: "
315                             + userStopResponse);
316                     return;
317                 }
318             } catch (Exception e) {
319                 Log.w(TAG, "Exception while stopping user " + mUserTracker.getUserId(), e);
320                 return;
321             }
322 
323             int displayId = mContext.getDisplayId();
324             try {
325                 mCarUserManager.startUser(
326                         new UserStartRequest.Builder(UserHandle.of(userId)).setDisplayId(
327                                 displayId).build(),
328                         Runnable::run,
329                         response -> {
330                             if (!response.isSuccess()) {
331                                 Log.e(TAG, "Could not start user " + userId + " on display "
332                                         + displayId + ". Response: " + response);
333                             }
334                         });
335             } catch (Exception e) {
336                 Log.w(TAG, "Exception while starting user " + userId + " on display " + displayId,
337                         e);
338             }
339         });
340     }
341 
logoutUser()342     private void logoutUser() {
343         mContext.sendBroadcastAsUser(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
344                 mUserTracker.getUserHandle());
345         AsyncFuture<UserSwitchResult> userSwitchResultFuture = mCarUserManager.logoutUser();
346         UserSwitchResult userSwitchResult;
347         try {
348             userSwitchResult = userSwitchResultFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
349         } catch (Exception e) {
350             Log.w(TAG, "Could not log out user.", e);
351             return;
352         }
353         if (userSwitchResult == null) {
354             Log.w(TAG, "Timed out while logging out user: " + TIMEOUT_MS + "ms");
355         } else if (!userSwitchResult.isSuccess()) {
356             Log.w(TAG, "Could not log out user: " + userSwitchResult);
357         }
358     }
359 
360     /**
361      * Finds the existing Guest user, or creates one if it doesn't exist.
362      *
363      * @param context App context
364      * @return UserInfo representing the Guest user
365      */
366     @Nullable
createNewOrFindExistingGuest(Context context)367     protected UserInfo createNewOrFindExistingGuest(Context context) {
368         AsyncFuture<UserCreationResult> future = mCarUserManager.createGuest(
369                 context.getString(com.android.internal.R.string.guest_name));
370         // CreateGuest will return null if a guest already exists.
371         UserInfo newGuest = getUserInfo(future);
372         if (newGuest != null) {
373             UserHelper.assignDefaultIcon(context, newGuest.getUserHandle());
374             return newGuest;
375         }
376         return mUserManager.findCurrentGuestUser();
377     }
378 
379     @Nullable
getUserInfo(AsyncFuture<UserCreationResult> future)380     private UserInfo getUserInfo(AsyncFuture<UserCreationResult> future) {
381         UserCreationResult userCreationResult;
382         try {
383             userCreationResult = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
384         } catch (Exception e) {
385             Log.w(TAG, "Could not create user.", e);
386             return null;
387         }
388         if (userCreationResult == null) {
389             Log.w(TAG, "Timed out while creating user: " + TIMEOUT_MS + "ms");
390             return null;
391         }
392         if (!userCreationResult.isSuccess() || userCreationResult.getUser() == null) {
393             Log.w(TAG, "Could not create user: " + userCreationResult);
394             return null;
395         }
396         return mUserManager.getUserInfo(userCreationResult.getUser().getIdentifier());
397     }
398 
getCircularAddUserIcon()399     private RoundedBitmapDrawable getCircularAddUserIcon() {
400         RoundedBitmapDrawable circleIcon = RoundedBitmapDrawableFactory.create(
401                 mContext.getResources(),
402                 UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.car_add_circle_round)));
403         circleIcon.setCircular(true);
404         return circleIcon;
405     }
406 
hasAddUserRestriction(UserHandle userHandle)407     private boolean hasAddUserRestriction(UserHandle userHandle) {
408         return mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_ADD_USER, userHandle);
409     }
410 
getMaxSupportedRealUsers()411     private int getMaxSupportedRealUsers() {
412         int maxSupportedUsers = UserManager.getMaxSupportedUsers();
413         if (UserManager.isHeadlessSystemUserMode()) {
414             maxSupportedUsers -= 1;
415         }
416         List<UserInfo> users = mUserManager.getAliveUsers();
417         // Count all users that are managed profiles of another user.
418         int managedProfilesCount = 0;
419         for (UserInfo user : users) {
420             if (user.isManagedProfile()) {
421                 managedProfilesCount++;
422             }
423         }
424         return maxSupportedUsers - managedProfilesCount;
425     }
426 
showMaxUserLimitReachedDialog()427     private void showMaxUserLimitReachedDialog() {
428         AlertDialog maxUsersDialog = new AlertDialog.Builder(mContext,
429                 com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
430                 .setTitle(R.string.profile_limit_reached_title)
431                 .setMessage(StringUtil.getIcuPluralsString(mContext, getMaxSupportedRealUsers(),
432                         R.string.profile_limit_reached_message))
433                 .setPositiveButton(android.R.string.ok, null)
434                 .create();
435         // Sets window flags for the SysUI dialog
436         applyCarSysUIDialogFlags(maxUsersDialog);
437         maxUsersDialog.show();
438     }
439 
showConfirmAddUserDialog()440     private void showConfirmAddUserDialog() {
441         String message = mContext.getString(R.string.user_add_user_message_setup)
442                 .concat(System.getProperty("line.separator"))
443                 .concat(System.getProperty("line.separator"))
444                 .concat(mContext.getString(R.string.user_add_user_message_update));
445         AlertDialog addUserDialog = new AlertDialog.Builder(mContext,
446                 com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
447                 .setTitle(R.string.user_add_profile_title)
448                 .setMessage(message)
449                 .setNegativeButton(android.R.string.cancel, null)
450                 .setPositiveButton(android.R.string.ok,
451                         (dialog, which) -> new AddNewUserTask().execute(
452                                 mContext.getString(R.string.car_new_user)))
453                 .create();
454         // Sets window flags for the SysUI dialog
455         applyCarSysUIDialogFlags(addUserDialog);
456         addUserDialog.show();
457     }
458 
applyCarSysUIDialogFlags(AlertDialog dialog)459     private void applyCarSysUIDialogFlags(AlertDialog dialog) {
460         Window window = dialog.getWindow();
461         window.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
462         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
463                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
464         window.getAttributes().setFitInsetsTypes(
465                 window.getAttributes().getFitInsetsTypes() & ~statusBars());
466     }
467 
onCarConnected(Car car)468     private void onCarConnected(Car car) {
469         if (DEBUG) {
470             Log.d(TAG, "car connected");
471         }
472         mCarUserManager = car.getCarManager(CarUserManager.class);
473         notifyChange();
474     }
475 
476     private class AddNewUserTask extends AsyncTask<String, Void, UserInfo> {
477         @Override
doInBackground(String... userNames)478         protected UserInfo doInBackground(String... userNames) {
479             AsyncFuture<UserCreationResult> future = mCarUserManager.createUser(userNames[0],
480                     /* flags= */ 0);
481             try {
482                 UserInfo user = getUserInfo(future);
483                 if (user != null) {
484                     UserHelper.setDefaultNonAdminRestrictions(mContext, user.getUserHandle(),
485                             /* enable= */ true);
486                     UserHelper.assignDefaultIcon(mContext, user.getUserHandle());
487                     return user;
488                 } else {
489                     Log.e(TAG, "Failed to create user in the background");
490                     return user;
491                 }
492             } catch (Exception e) {
493                 if (e instanceof InterruptedException) {
494                     Thread.currentThread().interrupt();
495                 }
496                 Log.e(TAG, "Error creating new user: ", e);
497             }
498             return null;
499         }
500 
501         @Override
onPreExecute()502         protected void onPreExecute() {
503             mPendingUserAdd = true;
504         }
505 
506         @Override
onPostExecute(UserInfo user)507         protected void onPostExecute(UserInfo user) {
508             mPendingUserAdd = false;
509             if (user != null) {
510                 switchUser(user.id);
511             }
512         }
513     }
514 }
515