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