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