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