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