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