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