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