1 /* 2 * Copyright (C) 2023 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 17 package com.android.systemui.car.userpicker; 18 19 import static android.car.CarOccupantZoneManager.INVALID_USER_ID; 20 import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED; 21 import static android.car.user.CarUserManager.lifecycleEventTypeToString; 22 import static android.view.Display.INVALID_DISPLAY; 23 24 import static com.android.systemui.car.userpicker.DialogManager.DIALOG_TYPE_ADDING_USER; 25 import static com.android.systemui.car.userpicker.DialogManager.DIALOG_TYPE_CONFIRM_ADD_USER; 26 import static com.android.systemui.car.userpicker.DialogManager.DIALOG_TYPE_CONFIRM_LOGOUT; 27 import static com.android.systemui.car.userpicker.DialogManager.DIALOG_TYPE_MAX_USER_COUNT_REACHED; 28 import static com.android.systemui.car.userpicker.DialogManager.DIALOG_TYPE_SWITCHING; 29 import static com.android.systemui.car.userpicker.HeaderState.HEADER_STATE_CHANGE_USER; 30 import static com.android.systemui.car.userpicker.HeaderState.HEADER_STATE_LOGOUT; 31 32 import android.annotation.IntDef; 33 import android.annotation.UserIdInt; 34 import android.app.ActivityManager; 35 import android.car.feature.Flags; 36 import android.car.user.UserCreationResult; 37 import android.content.Context; 38 import android.content.pm.UserInfo; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.os.Message; 42 import android.util.Log; 43 import android.util.Slog; 44 import android.view.View.OnClickListener; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.VisibleForTesting; 48 49 import com.android.internal.widget.LockPatternUtils; 50 import com.android.systemui.R; 51 import com.android.systemui.car.userpicker.UserEventManager.OnUpdateUsersListener; 52 import com.android.systemui.car.userpicker.UserRecord.OnClickListenerCreatorBase; 53 import com.android.systemui.car.userswitcher.UserIconProvider; 54 import com.android.systemui.settings.DisplayTracker; 55 56 import java.io.PrintWriter; 57 import java.lang.annotation.Retention; 58 import java.lang.annotation.RetentionPolicy; 59 import java.util.ArrayList; 60 import java.util.List; 61 import java.util.concurrent.ExecutorService; 62 import java.util.concurrent.Executors; 63 64 import javax.inject.Inject; 65 66 @UserPickerScope 67 final class UserPickerController { 68 private static final String TAG = UserPickerController.class.getSimpleName(); 69 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 70 71 private static final int REQ_SHOW_ADDING_DIALOG = 1; 72 private static final int REQ_DISMISS_ADDING_DIALOG = 2; 73 private static final int REQ_SHOW_SWITCHING_DIALOG = 3; 74 private static final int REQ_DISMISS_SWITCHING_DIALOG = 4; 75 private static final int REQ_FINISH_ACTIVITY = 5; 76 private static final int REQ_SHOW_SNACKBAR = 6; 77 78 @IntDef(prefix = { "REQ_" }, value = { 79 REQ_SHOW_ADDING_DIALOG, 80 REQ_DISMISS_ADDING_DIALOG, 81 REQ_SHOW_SWITCHING_DIALOG, 82 REQ_DISMISS_SWITCHING_DIALOG, 83 REQ_FINISH_ACTIVITY, 84 REQ_SHOW_SNACKBAR, 85 }) 86 @Retention(RetentionPolicy.SOURCE) 87 public @interface PresenterRequestType {} 88 89 private final CarServiceMediator mCarServiceMediator; 90 private final DialogManager mDialogManager; 91 private final SnackbarManager mSnackbarManager; 92 private final LockPatternUtils mLockPatternUtils; 93 private final ExecutorService mWorker; 94 private final DisplayTracker mDisplayTracker; 95 private final UserPickerSharedState mUserPickerSharedState; 96 97 private Context mContext; 98 private UserEventManager mUserEventManager; 99 private UserIconProvider mUserIconProvider; 100 private int mDisplayId; 101 private Callbacks mCallbacks; 102 private HeaderState mHeaderState; 103 104 private boolean mIsUserPickerClickable = true; 105 106 private String mDefaultGuestName; 107 private String mAddUserButtonName; 108 109 // Handler for main thread 110 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 111 @Override 112 public void handleMessage(@NonNull Message msg) { 113 super.handleMessage(msg); 114 switch (msg.what) { 115 case REQ_SHOW_ADDING_DIALOG: 116 mDialogManager.showDialog(DIALOG_TYPE_ADDING_USER); 117 break; 118 case REQ_DISMISS_ADDING_DIALOG: 119 mDialogManager.dismissDialog(DIALOG_TYPE_ADDING_USER); 120 break; 121 case REQ_SHOW_SWITCHING_DIALOG: 122 mDialogManager.showDialog(DIALOG_TYPE_SWITCHING); 123 break; 124 case REQ_DISMISS_SWITCHING_DIALOG: 125 mDialogManager.dismissDialog(DIALOG_TYPE_SWITCHING); 126 break; 127 case REQ_FINISH_ACTIVITY: 128 mCallbacks.onFinishRequested(); 129 break; 130 case REQ_SHOW_SNACKBAR: 131 mSnackbarManager.showSnackbar((String) msg.obj); 132 break; 133 } 134 } 135 }; 136 137 private OnUpdateUsersListener mUsersUpdateListener = (userId, userState) -> { 138 onUserUpdate(userId, userState); 139 }; 140 141 private Runnable mAddUserRunnable = () -> { 142 UserCreationResult result = mUserEventManager.createNewUser(); 143 runOnMainHandler(REQ_DISMISS_ADDING_DIALOG); 144 145 if (result != null && result.isSuccess()) { 146 int userId = result.getUser().getIdentifier(); 147 UserInfo newUserInfo = mUserEventManager.getUserInfo(userId); 148 UserRecord userRecord = UserRecord.create(newUserInfo, newUserInfo.name, 149 /* isStartGuestSession= */ false, /* isAddUser= */ false, 150 /* isForeground= */ false, 151 /* icon= */ mUserIconProvider.getRoundedUserIcon(userId), 152 /* listenerMaker */ new OnClickListenerCreator()); 153 mIsUserPickerClickable = false; 154 handleUserSelected(userRecord); 155 } else { 156 Slog.w(TAG, "Unsuccessful UserCreationResult:" + result); 157 // Show snack bar message for the failure of user creation. 158 runOnMainHandler(REQ_SHOW_SNACKBAR, 159 mContext.getString(R.string.create_user_failed_message)); 160 } 161 }; 162 163 @Inject UserPickerController(Context context, UserEventManager userEventManager, CarServiceMediator carServiceMediator, DialogManager dialogManager, SnackbarManager snackbarManager, DisplayTracker displayTracker, UserPickerSharedState userPickerSharedState, UserIconProvider userIconProvider)164 UserPickerController(Context context, UserEventManager userEventManager, 165 CarServiceMediator carServiceMediator, DialogManager dialogManager, 166 SnackbarManager snackbarManager, DisplayTracker displayTracker, 167 UserPickerSharedState userPickerSharedState, UserIconProvider userIconProvider) { 168 mContext = context; 169 mUserEventManager = userEventManager; 170 mCarServiceMediator = carServiceMediator; 171 mDialogManager = dialogManager; 172 mSnackbarManager = snackbarManager; 173 mLockPatternUtils = new LockPatternUtils(mContext); 174 mUserIconProvider = userIconProvider; 175 mDisplayTracker = displayTracker; 176 mUserPickerSharedState = userPickerSharedState; 177 mWorker = Executors.newSingleThreadExecutor(); 178 } 179 onConfigurationChanged()180 void onConfigurationChanged() { 181 updateTexts(); 182 updateUsers(); 183 } 184 onUserUpdate(int userId, int userState)185 private void onUserUpdate(int userId, int userState) { 186 if (DEBUG) { 187 Slog.d(TAG, "OnUsersUpdateListener: userId=" + userId 188 + " userState=" + lifecycleEventTypeToString(userState) 189 + " displayId=" + mDisplayId); 190 } 191 if (userState == USER_LIFECYCLE_EVENT_TYPE_UNLOCKED) { 192 if (mUserPickerSharedState.getUserLoginStarted(mDisplayId) == userId) { 193 if (DEBUG) { 194 Slog.d(TAG, "user " + userId + " unlocked. finish user picker." 195 + " displayId=" + mDisplayId); 196 } 197 mCallbacks.onFinishRequested(); 198 mUserPickerSharedState.resetUserLoginStarted(mDisplayId); 199 } 200 } 201 updateHeaderState(); 202 mCallbacks.onUpdateUsers(createUserRecords()); 203 } 204 updateHeaderState()205 private void updateHeaderState() { 206 // If a valid user is assigned to a display, show the change user state. Otherwise, show 207 // the logged out state. 208 int desiredState = mCarServiceMediator.getUserForDisplay(mDisplayId) != INVALID_USER_ID 209 ? HEADER_STATE_CHANGE_USER : HEADER_STATE_LOGOUT; 210 if (mHeaderState.getState() != desiredState) { 211 if (DEBUG) { 212 Slog.d(TAG, 213 "Change HeaderState to " + desiredState + " for displayId=" + mDisplayId); 214 } 215 mHeaderState.setState(desiredState); 216 } 217 } 218 updateTexts()219 private void updateTexts() { 220 mDefaultGuestName = mContext.getString(R.string.car_guest); 221 mAddUserButtonName = mContext.getString(R.string.car_add_user); 222 223 mDialogManager.updateTexts(mContext); 224 mCarServiceMediator.updateTexts(); 225 } 226 runOnMainHandler(@resenterRequestType int reqType)227 void runOnMainHandler(@PresenterRequestType int reqType) { 228 mHandler.sendMessage(mHandler.obtainMessage(reqType)); 229 } 230 runOnMainHandler(@resenterRequestType int reqType, Object params)231 void runOnMainHandler(@PresenterRequestType int reqType, Object params) { 232 mHandler.sendMessage(mHandler.obtainMessage(reqType, params)); 233 } 234 init(Callbacks callbacks, int displayId)235 void init(Callbacks callbacks, int displayId) { 236 mCallbacks = callbacks; 237 mDisplayId = displayId; 238 boolean isLoggedOutState = mCarServiceMediator.getUserForDisplay(mDisplayId) 239 == INVALID_USER_ID; 240 mHeaderState = new HeaderState(callbacks); 241 mHeaderState.setState(isLoggedOutState ? HEADER_STATE_LOGOUT : HEADER_STATE_CHANGE_USER); 242 mUserEventManager.registerOnUpdateUsersListener(mUsersUpdateListener, mDisplayId); 243 } 244 updateUsers()245 void updateUsers() { 246 mCallbacks.onUpdateUsers(createUserRecords()); 247 } 248 onDestroy()249 void onDestroy() { 250 if (DEBUG) { 251 Slog.d(TAG, "onDestroy: unregisterOnUsersUpdateListener. displayId=" + mDisplayId); 252 } 253 mUserPickerSharedState.resetUserLoginStarted(mDisplayId); 254 mUserEventManager.unregisterOnUpdateUsersListener(mDisplayId); 255 mUserEventManager.onDestroy(); 256 } 257 getOnClickListener(UserRecord userRecord)258 OnClickListener getOnClickListener(UserRecord userRecord) { 259 return holderView -> { 260 if (!mIsUserPickerClickable) { 261 return; 262 } 263 mIsUserPickerClickable = false; 264 // If the user wants to add a user, show dialog to confirm adding a user 265 if (userRecord != null && userRecord.mIsAddUser) { 266 if (mUserEventManager.isUserLimitReached()) { 267 mDialogManager.showDialog(DIALOG_TYPE_MAX_USER_COUNT_REACHED); 268 } else { 269 mDialogManager.showDialog(DIALOG_TYPE_CONFIRM_ADD_USER, 270 () -> startAddNewUser()); 271 } 272 mIsUserPickerClickable = true; 273 return; 274 } 275 handleUserSelected(userRecord); 276 }; 277 } 278 279 void screenOffDisplay() { 280 mCarServiceMediator.screenOffDisplay(mDisplayId); 281 } 282 283 void logoutUser() { 284 mIsUserPickerClickable = false; 285 int userId = mCarServiceMediator.getUserForDisplay(mDisplayId); 286 if (userId != INVALID_USER_ID) { 287 mDialogManager.showDialog( 288 DIALOG_TYPE_CONFIRM_LOGOUT, 289 () -> logoutUserInternal(userId), 290 () -> mIsUserPickerClickable = true); 291 } else { 292 mIsUserPickerClickable = true; 293 } 294 } 295 296 private void logoutUserInternal(int userId) { 297 mUserPickerSharedState.resetUserLoginStarted(mDisplayId); 298 mUserEventManager.stopUserUnchecked(userId, mDisplayId); 299 mUserEventManager.runUpdateUsersOnMainThread(userId, 0); 300 mIsUserPickerClickable = true; 301 } 302 303 @VisibleForTesting 304 List<UserRecord> createUserRecords() { 305 if (DEBUG) { 306 Slog.d(TAG, "createUserRecords. displayId=" + mDisplayId); 307 } 308 List<UserInfo> userInfos = mUserEventManager.getAliveUsers(); 309 List<UserRecord> userRecords = new ArrayList<>(userInfos.size()); 310 UserInfo foregroundUser = mUserEventManager.getCurrentForegroundUserInfo(); 311 312 if (mDisplayId == mDisplayTracker.getDefaultDisplayId()) { 313 if (mUserEventManager.isForegroundUserNotSwitchable(foregroundUser.getUserHandle())) { 314 userRecords.add(UserRecord.create(foregroundUser, /* name= */ foregroundUser.name, 315 /* isStartGuestSession= */ false, /* isAddUser= */ false, 316 /* isForeground= */ true, 317 /* icon= */ mUserIconProvider.getRoundedUserIcon(foregroundUser.id), 318 /* listenerMaker */ new OnClickListenerCreator(), 319 mLockPatternUtils.isSecure(foregroundUser.id), 320 /* isLoggedIn= */ true, /* loggedInDisplay= */ mDisplayId, 321 /* seatLocationName= */ mCarServiceMediator.getSeatString(mDisplayId), 322 /* isStopping= */ false)); 323 return userRecords; 324 } 325 } 326 327 for (int i = 0; i < userInfos.size(); i++) { 328 UserInfo userInfo = userInfos.get(i); 329 if (userInfo.isManagedProfile()) { 330 // Don't display guests or managed profile in the picker. 331 continue; 332 } 333 int loggedInDisplayId = mCarServiceMediator.getDisplayIdForUser(userInfo.id); 334 UserRecord record = UserRecord.create(userInfo, /* name= */ userInfo.name, 335 /* isStartGuestSession= */ false, /* isAddUser= */ false, 336 /* isForeground= */ userInfo.id == foregroundUser.id, 337 /* icon= */ mUserIconProvider.getRoundedUserIcon(userInfo.id), 338 /* listenerMaker */ new OnClickListenerCreator(), 339 /* isSecure= */ mLockPatternUtils.isSecure(userInfo.id), 340 /* isLoggedIn= */ loggedInDisplayId != INVALID_DISPLAY, 341 /* loggedInDisplay= */ loggedInDisplayId, 342 /* seatLocationName= */ mCarServiceMediator.getSeatString(loggedInDisplayId), 343 /* isStopping= */ mUserPickerSharedState.isStoppingUser(userInfo.id)); 344 userRecords.add(record); 345 346 if (DEBUG) { 347 Slog.d(TAG, "createUserRecord: userId=" + userInfo.id 348 + " logged-in=" + record.mIsLoggedIn 349 + " logged-in display=" + loggedInDisplayId 350 + " isStopping=" + record.mIsStopping); 351 } 352 } 353 354 // Add button for starting guest session. 355 userRecords.add(createStartGuestUserRecord()); 356 357 // Add add user record if the foreground user can add users 358 if (mUserEventManager.canForegroundUserAddUsers()) { 359 userRecords.add(createAddUserRecord()); 360 } 361 362 return userRecords; 363 } 364 365 /** 366 * Creates guest user record. 367 */ 368 private UserRecord createStartGuestUserRecord() { 369 boolean loggedIn = isGuestOnDisplay(); 370 int loggedInDisplay = loggedIn ? mDisplayId : INVALID_DISPLAY; 371 return UserRecord.create(/* info= */ null, /* name= */ mDefaultGuestName, 372 /* isStartGuestSession= */ true, /* isAddUser= */ false, 373 /* isForeground= */ false, 374 /* icon= */ mUserIconProvider.getRoundedGuestDefaultIcon(), 375 /* listenerMaker */ new OnClickListenerCreator(), 376 /* isSecure */ false, 377 loggedIn, loggedInDisplay, 378 /* seatLocationName= */mCarServiceMediator.getSeatString(loggedInDisplay), 379 /* isStopping= */ false); 380 } 381 382 /** 383 * Creates add user record. 384 */ 385 private UserRecord createAddUserRecord() { 386 return UserRecord.create(/* mInfo= */ null, /* mName= */ mAddUserButtonName, 387 /* mIsStartGuestSession= */ false, /* mIsAddUser= */ true, 388 /* mIsForeground= */ false, 389 /* mIcon= */ mContext.getDrawable(R.drawable.car_add_circle_round), 390 /* OnClickListenerMaker */ new OnClickListenerCreator()); 391 } 392 393 void handleUserSelected(UserRecord userRecord) { 394 if (userRecord == null) { 395 return; 396 } 397 mWorker.execute(() -> { 398 int userId = userRecord.mInfo != null ? userRecord.mInfo.id : INVALID_USER_ID; 399 400 // First, check login itself. 401 int prevUserId = mCarServiceMediator.getUserForDisplay(mDisplayId); 402 if ((userId != INVALID_USER_ID && userId == prevUserId) 403 || (userRecord.mIsStartGuestSession && isGuestUser(prevUserId))) { 404 runOnMainHandler(REQ_FINISH_ACTIVITY); 405 return; 406 } 407 408 boolean isFgUserStart = prevUserId == ActivityManager.getCurrentUser(); 409 410 // Second, check user has been already logged-in in another display or is stopping. 411 if ((userRecord.mIsLoggedIn && userRecord.mLoggedInDisplay != mDisplayId) 412 || mUserPickerSharedState.isStoppingUser(userId) 413 || (!Flags.supportsSecurePassengerUsers() && userRecord.mIsSecure 414 && !isFgUserStart)) { 415 String message; 416 if (userRecord.mIsStopping) { 417 message = mContext.getString(R.string.wait_for_until_stopped_message, 418 userRecord.mName); 419 } else if (!Flags.supportsSecurePassengerUsers() && userRecord.mIsSecure 420 && !isFgUserStart) { 421 message = mContext.getString(R.string.unavailable_secure_user_message); 422 } else { 423 message = mContext.getString(R.string.already_logged_in_message, 424 userRecord.mName, userRecord.mSeatLocationName); 425 } 426 runOnMainHandler(REQ_SHOW_SNACKBAR, message); 427 mIsUserPickerClickable = true; 428 return; 429 } 430 431 // Finally, start user if it has no problem. 432 boolean result = false; 433 try { 434 if (userRecord.mIsStartGuestSession) { 435 runOnMainHandler(REQ_SHOW_SWITCHING_DIALOG); 436 UserCreationResult creationResult = mUserEventManager.createGuest(); 437 if (creationResult == null || !creationResult.isSuccess()) { 438 if (creationResult == null) { 439 Slog.w(TAG, "Guest UserCreationResult is null"); 440 } else if (!creationResult.isSuccess()) { 441 Slog.w(TAG, "Unsuccessful guest UserCreationResult: " 442 + creationResult.toString()); 443 } 444 445 runOnMainHandler(REQ_DISMISS_SWITCHING_DIALOG); 446 // Show snack bar message for the failure of guest creation. 447 runOnMainHandler(REQ_SHOW_SNACKBAR, 448 mContext.getString(R.string.guest_creation_failed_message)); 449 return; 450 } 451 userId = creationResult.getUser().getIdentifier(); 452 } 453 454 if (!mUserPickerSharedState.setUserLoginStarted(mDisplayId, userId)) { 455 return; 456 } 457 458 if (!isFgUserStart && !stopUserAssignedToDisplay(prevUserId)) { 459 return; 460 } 461 462 runOnMainHandler(REQ_SHOW_SWITCHING_DIALOG); 463 result = mUserEventManager.startUserForDisplay(prevUserId, userId, mDisplayId, 464 isFgUserStart); 465 } finally { 466 mIsUserPickerClickable = !result; 467 if (result) { 468 if (mLockPatternUtils.isSecure(userId) 469 || mUserEventManager.isUserRunningUnlocked(userId)) { 470 if (DEBUG) { 471 Slog.d(TAG, "handleUserSelected: result true, isUserRunningUnlocked=" 472 + mUserEventManager.isUserRunningUnlocked(userId) 473 + " isSecure=" + mLockPatternUtils.isSecure(userId)); 474 } 475 runOnMainHandler(REQ_FINISH_ACTIVITY); 476 } 477 } else { 478 runOnMainHandler(REQ_DISMISS_SWITCHING_DIALOG); 479 mUserPickerSharedState.resetUserLoginStarted(mDisplayId); 480 } 481 } 482 }); 483 } 484 485 boolean stopUserAssignedToDisplay(@UserIdInt int prevUserId) { 486 // First, check whether the previous user is assigned to this display. 487 if (prevUserId == INVALID_USER_ID) { 488 Slog.i(TAG, "There is no user assigned for this display " + mDisplayId); 489 return true; 490 } 491 492 // Second, is starting user same with current user? 493 int currentUser = ActivityManager.getCurrentUser(); 494 if (prevUserId == currentUser) { 495 Slog.w(TAG, "Can not stop current user " + currentUser); 496 return false; 497 } 498 499 // Finally, we don't need to stop user if the user is already stopped. 500 if (!mUserEventManager.isUserRunning(prevUserId)) { 501 if (DEBUG) { 502 Slog.d(TAG, "User " + prevUserId + " is already stopping or stopped"); 503 } 504 return true; 505 } 506 507 runOnMainHandler(REQ_SHOW_SWITCHING_DIALOG); 508 return mUserEventManager.stopUserUnchecked(prevUserId, mDisplayId); 509 } 510 511 // This method is called only when creating user record. 512 boolean isGuestOnDisplay() { 513 int userId = mCarServiceMediator.getUserForDisplay(mDisplayId); 514 return isGuestUser(userId); 515 } 516 517 private boolean isGuestUser(@UserIdInt int userId) { 518 UserInfo userInfo = mUserEventManager.getUserInfo(userId); 519 return userInfo == null ? false : userInfo.isGuest(); 520 } 521 522 void startAddNewUser() { 523 runOnMainHandler(REQ_SHOW_ADDING_DIALOG); 524 mWorker.execute(mAddUserRunnable); 525 } 526 527 void dump(@NonNull PrintWriter pw) { 528 pw.println(" " + getClass().getSimpleName() + ":"); 529 if (mHeaderState.getState() == HEADER_STATE_CHANGE_USER) { 530 int loggedInUserId = mCarServiceMediator.getUserForDisplay(mDisplayId); 531 pw.println(" Logged-in user : " + loggedInUserId 532 + (isGuestUser(loggedInUserId) ? "(guest)" : "")); 533 } 534 pw.println(" mHeaderState=" + mHeaderState.toString()); 535 pw.println(" mIsUserPickerClickable=" + mIsUserPickerClickable); 536 } 537 538 class OnClickListenerCreator extends OnClickListenerCreatorBase { 539 @Override 540 OnClickListener createOnClickListenerWithUserRecord() { 541 return getOnClickListener(mUserRecord); 542 } 543 } 544 545 interface Callbacks { 546 void onUpdateUsers(List<UserRecord> users); 547 void onHeaderStateChanged(HeaderState headerState); 548 void onFinishRequested(); 549 } 550 } 551