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