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