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