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