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