• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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