• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.userswitcher;
18 
19 import static android.content.DialogInterface.BUTTON_NEGATIVE;
20 import static android.content.DialogInterface.BUTTON_POSITIVE;
21 import static android.os.UserManager.DISALLOW_ADD_USER;
22 import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
23 import static android.view.WindowInsets.Type.statusBars;
24 import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
25 
26 import static com.android.systemui.car.users.CarSystemUIUserUtil.getCurrentUserHandle;
27 
28 import android.annotation.IntDef;
29 import android.annotation.Nullable;
30 import android.annotation.UserIdInt;
31 import android.app.AlertDialog;
32 import android.app.AlertDialog.Builder;
33 import android.app.Dialog;
34 import android.car.user.CarUserManager;
35 import android.car.user.UserCreationResult;
36 import android.car.user.UserSwitchResult;
37 import android.car.util.concurrent.AsyncFuture;
38 import android.content.BroadcastReceiver;
39 import android.content.Context;
40 import android.content.DialogInterface;
41 import android.content.Intent;
42 import android.content.IntentFilter;
43 import android.content.pm.UserInfo;
44 import android.content.res.Resources;
45 import android.graphics.Rect;
46 import android.graphics.drawable.Drawable;
47 import android.os.AsyncTask;
48 import android.os.UserHandle;
49 import android.os.UserManager;
50 import android.sysprop.CarProperties;
51 import android.util.AttributeSet;
52 import android.util.Log;
53 import android.view.LayoutInflater;
54 import android.view.View;
55 import android.view.ViewGroup;
56 import android.view.Window;
57 import android.view.WindowManager;
58 import android.widget.TextView;
59 
60 import androidx.recyclerview.widget.GridLayoutManager;
61 import androidx.recyclerview.widget.RecyclerView;
62 
63 import com.android.car.admin.ui.UserAvatarView;
64 import com.android.car.internal.user.UserHelper;
65 import com.android.settingslib.utils.StringUtil;
66 import com.android.systemui.R;
67 import com.android.systemui.settings.UserTracker;
68 
69 import java.lang.annotation.Retention;
70 import java.lang.annotation.RetentionPolicy;
71 import java.util.ArrayList;
72 import java.util.List;
73 import java.util.concurrent.ExecutorService;
74 import java.util.concurrent.Executors;
75 import java.util.concurrent.TimeUnit;
76 import java.util.stream.Collectors;
77 
78 /**
79  * Displays a GridLayout with icons for the users in the system to allow switching between users.
80  * One of the uses of this is for the lock screen in auto.
81  */
82 public class UserGridRecyclerView extends RecyclerView {
83     private static final String TAG = UserGridRecyclerView.class.getSimpleName();
84     private static final int TIMEOUT_MS = CarProperties.user_hal_timeout().orElse(5_000) + 500;
85 
86     private final ExecutorService mWorker;
87 
88     @Nullable
89     private UserTracker mUserTracker;
90     private UserSelectionListener mUserSelectionListener;
91     private UserAdapter mAdapter;
92     private CarUserManager mCarUserManager;
93     private UserManager mUserManager;
94     private Context mContext;
95     private UserIconProvider mUserIconProvider;
96 
97     private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() {
98         @Override
99         public void onReceive(Context context, Intent intent) {
100             onUsersUpdate();
101         }
102     };
103 
UserGridRecyclerView(Context context, AttributeSet attrs)104     public UserGridRecyclerView(Context context, AttributeSet attrs) {
105         super(context, attrs);
106         mContext = context;
107         mUserManager = UserManager.get(mContext);
108         mWorker = Executors.newSingleThreadExecutor();
109 
110         addItemDecoration(new ItemSpacingDecoration(mContext.getResources().getDimensionPixelSize(
111                 R.dimen.car_user_switcher_vertical_spacing_between_users)));
112     }
113 
114     /**
115      * Register listener for any update to the users
116      */
117     @Override
onFinishInflate()118     public void onFinishInflate() {
119         super.onFinishInflate();
120         registerForUserEvents();
121     }
122 
123     /**
124      * Unregisters listener checking for any change to the users
125      */
126     @Override
onDetachedFromWindow()127     public void onDetachedFromWindow() {
128         super.onDetachedFromWindow();
129         unregisterForUserEvents();
130     }
131 
132     /**
133      * Initializes the adapter that populates the grid layout
134      */
buildAdapter()135     public void buildAdapter() {
136         List<UserRecord> userRecords = createUserRecords(getUsersForUserGrid());
137         mAdapter = new UserAdapter(mContext, userRecords);
138         super.setAdapter(mAdapter);
139     }
140 
getUsersForUserGrid()141     private List<UserInfo> getUsersForUserGrid() {
142         return mUserManager.getAliveUsers()
143                 .stream()
144                 .filter(userInfo -> userInfo.supportsSwitchTo() && userInfo.isFull())
145                 .sorted((u1, u2) -> Long.signum(u1.creationTime - u2.creationTime))
146                 .collect(Collectors.toList());
147     }
148 
createUserRecords(List<UserInfo> userInfoList)149     private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) {
150         int fgUserId = getCurrentUserId();
151         UserHandle fgUserHandle = UserHandle.of(fgUserId);
152         List<UserRecord> userRecords = new ArrayList<>();
153 
154         // If the foreground user CANNOT switch to other users, only display the foreground user.
155         if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) {
156             userRecords.add(createForegroundUserRecord());
157             return userRecords;
158         }
159 
160         for (UserInfo userInfo : userInfoList) {
161             if (userInfo.isGuest()) {
162                 // Don't display guests in the switcher.
163                 continue;
164             }
165 
166             boolean isForeground = fgUserId == userInfo.id;
167             UserRecord record = new UserRecord(userInfo,
168                     isForeground ? UserRecord.FOREGROUND_USER : UserRecord.BACKGROUND_USER);
169             userRecords.add(record);
170         }
171 
172         // Add button for starting guest session.
173         userRecords.add(createStartGuestUserRecord());
174 
175         // Add add user record if the foreground user can add users
176         if (!mUserManager.hasUserRestriction(DISALLOW_ADD_USER, fgUserHandle)) {
177             userRecords.add(createAddUserRecord());
178         }
179 
180         return userRecords;
181     }
182 
createForegroundUserRecord()183     private UserRecord createForegroundUserRecord() {
184         return new UserRecord(mUserManager.getUserInfo(getCurrentUserId()),
185                 UserRecord.FOREGROUND_USER);
186     }
187 
188     /**
189      * Create guest user record
190      */
createStartGuestUserRecord()191     private UserRecord createStartGuestUserRecord() {
192         return new UserRecord(null /* userInfo */, UserRecord.START_GUEST);
193     }
194 
195     /**
196      * Create add user record
197      */
createAddUserRecord()198     private UserRecord createAddUserRecord() {
199         return new UserRecord(null /* userInfo */, UserRecord.ADD_USER);
200     }
201 
setUserTracker(UserTracker userTracker)202     public void setUserTracker(UserTracker userTracker) {
203         mUserTracker = userTracker;
204     }
205 
setUserIconProvider(UserIconProvider userIconProvider)206     public void setUserIconProvider(UserIconProvider userIconProvider) {
207         mUserIconProvider = userIconProvider;
208     }
209 
setUserSelectionListener(UserSelectionListener userSelectionListener)210     public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
211         mUserSelectionListener = userSelectionListener;
212     }
213 
214     /** Sets a {@link CarUserManager}. */
setCarUserManager(CarUserManager carUserManager)215     public void setCarUserManager(CarUserManager carUserManager) {
216         mCarUserManager = carUserManager;
217     }
218 
getCurrentUserId()219     private int getCurrentUserId() {
220         return getCurrentUserHandle(mContext, mUserTracker).getIdentifier();
221     }
222 
onUsersUpdate()223     private void onUsersUpdate() {
224         if (mAdapter == null) {
225             return;
226         }
227         mAdapter.clearUsers();
228         mAdapter.updateUsers(createUserRecords(getUsersForUserGrid()));
229         mAdapter.notifyDataSetChanged();
230     }
231 
registerForUserEvents()232     private void registerForUserEvents() {
233         IntentFilter filter = new IntentFilter();
234         filter.addAction(Intent.ACTION_USER_REMOVED);
235         filter.addAction(Intent.ACTION_USER_ADDED);
236         filter.addAction(Intent.ACTION_USER_INFO_CHANGED);
237         filter.addAction(Intent.ACTION_USER_SWITCHED);
238         mContext.registerReceiverAsUser(
239                 mUserUpdateReceiver,
240                 UserHandle.ALL, // Necessary because CarSystemUi lives in User 0
241                 filter,
242                 /* broadcastPermission= */ null,
243                 /* scheduler= */ null);
244     }
245 
unregisterForUserEvents()246     private void unregisterForUserEvents() {
247         mContext.unregisterReceiver(mUserUpdateReceiver);
248     }
249 
250     /**
251      * Adapter to populate the grid layout with the available user profiles
252      */
253     public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder>
254             implements Dialog.OnClickListener, Dialog.OnCancelListener {
255 
256         private final Context mContext;
257         private Context mKeyguardDialogWindowContext;
258         private List<UserRecord> mUsers;
259         private final Resources mRes;
260         private final String mGuestName;
261         private final String mNewUserName;
262         // View that holds the add user button.  Used to enable/disable the view
263         private View mAddUserView;
264         // User record for the add user.  Need to call notifyUserSelected only if the user
265         // confirms adding a user
266         private UserRecord mAddUserRecord;
267 
UserAdapter(Context context, List<UserRecord> users)268         public UserAdapter(Context context, List<UserRecord> users) {
269             mRes = context.getResources();
270             mContext = context;
271             updateUsers(users);
272             mGuestName = mRes.getString(com.android.internal.R.string.guest_name);
273             mNewUserName = mRes.getString(R.string.car_new_user);
274         }
275 
276         /**
277          * Clears list of user records.
278          */
clearUsers()279         public void clearUsers() {
280             mUsers.clear();
281         }
282 
283         /**
284          * Updates list of user records.
285          */
updateUsers(List<UserRecord> users)286         public void updateUsers(List<UserRecord> users) {
287             mUsers = users;
288         }
289 
290         @Override
onCreateViewHolder(ViewGroup parent, int viewType)291         public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
292             View view = LayoutInflater.from(mContext)
293                     .inflate(R.layout.car_fullscreen_user_pod, parent, false);
294             view.setAlpha(1f);
295             view.bringToFront();
296             return new UserAdapterViewHolder(view);
297         }
298 
299         @Override
onBindViewHolder(UserAdapterViewHolder holder, int position)300         public void onBindViewHolder(UserAdapterViewHolder holder, int position) {
301             UserRecord userRecord = mUsers.get(position);
302 
303             Drawable roundedIcon = getRoundedUserRecordIcon(userRecord);
304             if (roundedIcon != null) {
305                 if (userRecord.mInfo != null) {
306                     // User might have badges (like managed user)
307                     holder.mUserAvatarImageView.setDrawableWithBadge(roundedIcon,
308                             userRecord.mInfo.id);
309                 } else {
310                     // Guest or "Add User" don't have badges
311                     holder.mUserAvatarImageView.setDrawable(roundedIcon);
312                 }
313             } else {
314                 Log.e(TAG, "Unable to get user icon");
315             }
316 
317             holder.mUserNameTextView.setText(getUserRecordName(userRecord));
318 
319             holder.mView.setOnClickListener(v -> {
320                 if (userRecord == null) {
321                     return;
322                 }
323 
324                 switch (userRecord.mType) {
325                     case UserRecord.START_GUEST:
326                         notifyUserSelected(userRecord);
327                         UserInfo guest = createNewOrFindExistingGuest(mContext);
328                         if (guest != null) {
329                             switchUser(guest.id);
330                         }
331                         break;
332                     case UserRecord.ADD_USER:
333                         // If the user wants to add a user, show dialog to confirm adding a user
334                         // Disable button so it cannot be clicked multiple times
335                         mAddUserView = holder.mView;
336                         mAddUserView.setEnabled(false);
337                         mAddUserRecord = userRecord;
338 
339                         handleAddUserClicked();
340                         break;
341                     default:
342                         // If the user doesn't want to be a guest or add a user, switch to the user
343                         // selected
344                         notifyUserSelected(userRecord);
345                         switchUser(userRecord.mInfo.id);
346                 }
347             });
348 
349         }
350 
handleAddUserClicked()351         private void handleAddUserClicked() {
352             if (!mUserManager.canAddMoreUsers()) {
353                 mAddUserView.setEnabled(true);
354                 showMaxUserLimitReachedDialog();
355             } else {
356                 showConfirmAddUserDialog();
357             }
358         }
359 
360         /**
361          * Get the maximum number of real (non-guest, non-managed profile) users that can be created
362          * on the device. This is a dynamic value and it decreases with the increase of the number
363          * of managed profiles on the device.
364          *
365          * <p> It excludes system user in headless system user model.
366          *
367          * @return Maximum number of real users that can be created.
368          */
getMaxSupportedRealUsers()369         private int getMaxSupportedRealUsers() {
370             int maxSupportedUsers = UserManager.getMaxSupportedUsers();
371             if (UserManager.isHeadlessSystemUserMode()) {
372                 maxSupportedUsers -= 1;
373             }
374 
375             List<UserInfo> users = mUserManager.getAliveUsers();
376 
377             // Count all users that are managed profiles of another user.
378             int managedProfilesCount = 0;
379             for (UserInfo user : users) {
380                 if (user.isManagedProfile()) {
381                     managedProfilesCount++;
382                 }
383             }
384 
385             return maxSupportedUsers - managedProfilesCount;
386         }
387 
showMaxUserLimitReachedDialog()388         private void showMaxUserLimitReachedDialog() {
389             AlertDialog maxUsersDialog = new Builder(mContext,
390                     com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
391                     .setTitle(R.string.profile_limit_reached_title)
392                     .setMessage(StringUtil.getIcuPluralsString(mContext, getMaxSupportedRealUsers(),
393                             R.string.profile_limit_reached_message))
394                     .setPositiveButton(android.R.string.ok, null)
395                     .create();
396             // Sets window flags for the SysUI dialog
397             applyCarSysUIDialogFlags(maxUsersDialog);
398             maxUsersDialog.show();
399         }
400 
showConfirmAddUserDialog()401         private void showConfirmAddUserDialog() {
402             String message = mRes.getString(R.string.user_add_user_message_setup)
403                     .concat(System.getProperty("line.separator"))
404                     .concat(System.getProperty("line.separator"))
405                     .concat(mRes.getString(R.string.user_add_user_message_update));
406 
407             AlertDialog addUserDialog = new Builder(getKeyguardDialogWindowContext(),
408                     com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
409                     .setTitle(R.string.user_add_profile_title)
410                     .setMessage(message)
411                     .setNegativeButton(android.R.string.cancel, this)
412                     .setPositiveButton(android.R.string.ok, this)
413                     .setOnCancelListener(this)
414                     .create();
415             // Sets window flags for the SysUI dialog
416             applyCarSysUIDialogFlags(addUserDialog);
417             addUserDialog.show();
418         }
419 
applyCarSysUIDialogFlags(AlertDialog dialog)420         private void applyCarSysUIDialogFlags(AlertDialog dialog) {
421             final Window window = dialog.getWindow();
422             window.setType(TYPE_KEYGUARD_DIALOG);
423             window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
424                     | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
425             window.getAttributes().setFitInsetsTypes(
426                     window.getAttributes().getFitInsetsTypes() & ~statusBars());
427         }
428 
getKeyguardDialogWindowContext()429         private Context getKeyguardDialogWindowContext() {
430             if (mKeyguardDialogWindowContext == null) {
431                 mKeyguardDialogWindowContext = mContext.createWindowContext(TYPE_KEYGUARD_DIALOG,
432                         /* options= */ null);
433             }
434             return mKeyguardDialogWindowContext;
435         }
436 
notifyUserSelected(UserRecord userRecord)437         private void notifyUserSelected(UserRecord userRecord) {
438             // Notify the listener which user was selected
439             if (mUserSelectionListener != null) {
440                 mUserSelectionListener.onUserSelected(userRecord);
441             }
442         }
443 
getRoundedUserRecordIcon(UserRecord userRecord)444         private Drawable getRoundedUserRecordIcon(UserRecord userRecord) {
445             if (mUserIconProvider == null) {
446                 return null;
447             }
448 
449             Drawable roundedIcon;
450             switch (userRecord.mType) {
451                 case UserRecord.START_GUEST:
452                     roundedIcon = mUserIconProvider.getRoundedGuestDefaultIcon();
453                     break;
454                 case UserRecord.ADD_USER:
455                     roundedIcon = mUserIconProvider.getRoundedAddUserIcon();
456                     break;
457                 default:
458                     roundedIcon = mUserIconProvider.getRoundedUserIcon(userRecord.mInfo.id);
459                     break;
460             }
461             return roundedIcon;
462         }
463 
getUserRecordName(UserRecord userRecord)464         private String getUserRecordName(UserRecord userRecord) {
465             String recordName;
466             switch (userRecord.mType) {
467                 case UserRecord.START_GUEST:
468                     recordName = mContext.getString(com.android.internal.R.string.guest_name);
469                     break;
470                 case UserRecord.ADD_USER:
471                     recordName = mContext.getString(R.string.car_add_user);
472                     break;
473                 default:
474                     recordName = userRecord.mInfo.name;
475                     break;
476             }
477             return recordName;
478         }
479 
480         /**
481          * Finds the existing Guest user, or creates one if it doesn't exist.
482          * @param context App context
483          * @return UserInfo representing the Guest user
484          */
485         @Nullable
createNewOrFindExistingGuest(Context context)486         public UserInfo createNewOrFindExistingGuest(Context context) {
487             AsyncFuture<UserCreationResult> future = mCarUserManager.createGuest(mGuestName);
488             // CreateGuest will return null if a guest already exists.
489             UserInfo newGuest = getUserInfo(future);
490             if (newGuest != null) {
491                 UserHelper.assignDefaultIcon(context, newGuest.getUserHandle());
492                 return newGuest;
493             }
494 
495             return mUserManager.findCurrentGuestUser();
496         }
497 
498         @Override
onClick(DialogInterface dialog, int which)499         public void onClick(DialogInterface dialog, int which) {
500             if (which == BUTTON_POSITIVE) {
501                 new AddNewUserTask().execute(mNewUserName);
502             } else if (which == BUTTON_NEGATIVE) {
503                 // Enable the add button only if cancel
504                 if (mAddUserView != null) {
505                     mAddUserView.setEnabled(true);
506                 }
507             }
508         }
509 
510         @Override
onCancel(DialogInterface dialog)511         public void onCancel(DialogInterface dialog) {
512             // Enable the add button again if user cancels dialog by clicking outside the dialog
513             if (mAddUserView != null) {
514                 mAddUserView.setEnabled(true);
515             }
516         }
517 
518         @Nullable
getUserInfo(AsyncFuture<UserCreationResult> future)519         private UserInfo getUserInfo(AsyncFuture<UserCreationResult> future) {
520             UserCreationResult userCreationResult;
521             try {
522                 userCreationResult = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
523             } catch (Exception e) {
524                 Log.w(TAG, "Could not create user.", e);
525                 return null;
526             }
527 
528             if (userCreationResult == null) {
529                 Log.w(TAG, "Timed out while creating user: " + TIMEOUT_MS + "ms");
530                 return null;
531             }
532             if (!userCreationResult.isSuccess() || userCreationResult.getUser() == null) {
533                 Log.w(TAG, "Could not create user: " + userCreationResult);
534                 return null;
535             }
536 
537             return mUserManager.getUserInfo(userCreationResult.getUser().getIdentifier());
538         }
539 
switchUser(@serIdInt int userId)540         private void switchUser(@UserIdInt int userId) {
541             mWorker.execute(() -> {
542                 AsyncFuture<UserSwitchResult> userSwitchResultFuture =
543                         mCarUserManager.switchUser(userId);
544                 UserSwitchResult userSwitchResult;
545                 try {
546                     userSwitchResult = userSwitchResultFuture.get(TIMEOUT_MS,
547                             TimeUnit.MILLISECONDS);
548                 } catch (Exception e) {
549                     Log.e(TAG, "Could not switch user.", e);
550                     return;
551                 }
552 
553                 if (userSwitchResult == null) {
554                     Log.e(TAG, "Timed out while switching user: " + TIMEOUT_MS + "ms");
555                     return;
556                 }
557                 if (!userSwitchResult.isSuccess()) {
558                     Log.e(TAG, "Could not switch user: " + userSwitchResult);
559                     return;
560                 }
561                 Log.v(TAG, "Switched to user " + userId + " successfully");
562             });
563         }
564 
565         // TODO(b/161539497): Replace AsyncTask with standard {@link java.util.concurrent} code.
566         private class AddNewUserTask extends AsyncTask<String, Void, UserInfo> {
567 
568             @Override
doInBackground(String... userNames)569             protected UserInfo doInBackground(String... userNames) {
570                 AsyncFuture<UserCreationResult> future = mCarUserManager.createUser(userNames[0],
571                         /* flags= */ 0);
572                 try {
573                     UserInfo user = getUserInfo(future);
574                     if (user != null) {
575                         UserHelper.setDefaultNonAdminRestrictions(mContext, user.getUserHandle(),
576                                 /* enable= */ true);
577                         UserHelper.assignDefaultIcon(mContext, user.getUserHandle());
578                         mAddUserRecord = new UserRecord(user, UserRecord.ADD_USER);
579                         return user;
580                     } else {
581                         Log.e(TAG, "Failed to create user in the background");
582                         return user;
583                     }
584                 } catch (Exception e) {
585                     if (e instanceof InterruptedException) {
586                         Thread.currentThread().interrupt();
587                     }
588                     Log.e(TAG, "Error creating new user: ", e);
589                 }
590                 return null;
591             }
592 
593             @Override
onPreExecute()594             protected void onPreExecute() {
595             }
596 
597             @Override
onPostExecute(UserInfo user)598             protected void onPostExecute(UserInfo user) {
599                 if (user != null) {
600                     notifyUserSelected(mAddUserRecord);
601                     mAddUserView.setEnabled(true);
602                     switchUser(user.id);
603                 }
604                 if (mAddUserView != null) {
605                     mAddUserView.setEnabled(true);
606                 }
607             }
608         }
609 
610         @Override
getItemCount()611         public int getItemCount() {
612             return mUsers.size();
613         }
614 
615         /**
616          * An extension of {@link RecyclerView.ViewHolder} that also houses the user name and the
617          * user avatar.
618          */
619         public class UserAdapterViewHolder extends RecyclerView.ViewHolder {
620 
621             public UserAvatarView mUserAvatarImageView;
622             public TextView mUserNameTextView;
623             public View mView;
624 
UserAdapterViewHolder(View view)625             public UserAdapterViewHolder(View view) {
626                 super(view);
627                 mView = view;
628                 mUserAvatarImageView = view.findViewById(R.id.user_avatar);
629                 mUserNameTextView = view.findViewById(R.id.user_name);
630             }
631         }
632     }
633 
634     /**
635      * Object wrapper class for the userInfo.  Use it to distinguish if a profile is a
636      * guest profile, add user profile, or the foreground user.
637      */
638     public static final class UserRecord {
639         public final UserInfo mInfo;
640         public final @UserRecordType int mType;
641 
642         public static final int START_GUEST = 0;
643         public static final int ADD_USER = 1;
644         public static final int FOREGROUND_USER = 2;
645         public static final int BACKGROUND_USER = 3;
646 
647         @IntDef({START_GUEST, ADD_USER, FOREGROUND_USER, BACKGROUND_USER})
648         @Retention(RetentionPolicy.SOURCE)
649         public @interface UserRecordType{}
650 
UserRecord(@ullable UserInfo userInfo, @UserRecordType int recordType)651         public UserRecord(@Nullable UserInfo userInfo, @UserRecordType int recordType) {
652             mInfo = userInfo;
653             mType = recordType;
654         }
655     }
656 
657     /**
658      * Listener used to notify when a user has been selected
659      */
660     interface UserSelectionListener {
661 
onUserSelected(UserRecord record)662         void onUserSelected(UserRecord record);
663     }
664 
665     /**
666      * A {@link RecyclerView.ItemDecoration} that will add spacing between each item in the
667      * RecyclerView that it is added to.
668      */
669     private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
670         private int mItemSpacing;
671 
ItemSpacingDecoration(int itemSpacing)672         private ItemSpacingDecoration(int itemSpacing) {
673             mItemSpacing = itemSpacing;
674         }
675 
676         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)677         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
678                 RecyclerView.State state) {
679             super.getItemOffsets(outRect, view, parent, state);
680             int position = parent.getChildAdapterPosition(view);
681 
682             // Skip offset for last item except for GridLayoutManager.
683             if (position == state.getItemCount() - 1
684                     && !(parent.getLayoutManager() instanceof GridLayoutManager)) {
685                 return;
686             }
687 
688             outRect.bottom = mItemSpacing;
689         }
690     }
691 }
692