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