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.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.internal.util.UserIcons;
66 import com.android.systemui.R;
67 
68 import java.lang.annotation.Retention;
69 import java.lang.annotation.RetentionPolicy;
70 import java.util.ArrayList;
71 import java.util.List;
72 import java.util.concurrent.TimeUnit;
73 import java.util.stream.Collectors;
74 
75 /**
76  * Displays a GridLayout with icons for the users in the system to allow switching between users.
77  * One of the uses of this is for the lock screen in auto.
78  */
79 public class UserGridRecyclerView extends RecyclerView {
80     private static final String TAG = UserGridRecyclerView.class.getSimpleName();
81     private static final int TIMEOUT_MS = CarProperties.user_hal_timeout().orElse(5_000) + 500;
82 
83     private UserSelectionListener mUserSelectionListener;
84     private UserAdapter mAdapter;
85     private CarUserManager mCarUserManager;
86     private UserManager mUserManager;
87     private Context mContext;
88     private UserIconProvider mUserIconProvider;
89 
90     private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() {
91         @Override
92         public void onReceive(Context context, Intent intent) {
93             onUsersUpdate();
94         }
95     };
96 
UserGridRecyclerView(Context context, AttributeSet attrs)97     public UserGridRecyclerView(Context context, AttributeSet attrs) {
98         super(context, attrs);
99         mContext = context;
100         mUserManager = UserManager.get(mContext);
101         mUserIconProvider = new UserIconProvider();
102 
103         addItemDecoration(new ItemSpacingDecoration(mContext.getResources().getDimensionPixelSize(
104                 R.dimen.car_user_switcher_vertical_spacing_between_users)));
105     }
106 
107     /**
108      * Register listener for any update to the users
109      */
110     @Override
onFinishInflate()111     public void onFinishInflate() {
112         super.onFinishInflate();
113         registerForUserEvents();
114     }
115 
116     /**
117      * Unregisters listener checking for any change to the users
118      */
119     @Override
onDetachedFromWindow()120     public void onDetachedFromWindow() {
121         super.onDetachedFromWindow();
122         unregisterForUserEvents();
123     }
124 
125     /**
126      * Initializes the adapter that populates the grid layout
127      */
buildAdapter()128     public void buildAdapter() {
129         List<UserRecord> userRecords = createUserRecords(getUsersForUserGrid());
130         mAdapter = new UserAdapter(mContext, userRecords);
131         super.setAdapter(mAdapter);
132     }
133 
getUsersForUserGrid()134     private List<UserInfo> getUsersForUserGrid() {
135         return mUserManager.getAliveUsers()
136                 .stream()
137                 .filter(UserInfo::supportsSwitchToByUser)
138                 .collect(Collectors.toList());
139     }
140 
createUserRecords(List<UserInfo> userInfoList)141     private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) {
142         int fgUserId = ActivityManager.getCurrentUser();
143         UserHandle fgUserHandle = UserHandle.of(fgUserId);
144         List<UserRecord> userRecords = new ArrayList<>();
145 
146         // If the foreground user CANNOT switch to other users, only display the foreground user.
147         if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) {
148             userRecords.add(createForegroundUserRecord());
149             return userRecords;
150         }
151 
152         for (UserInfo userInfo : userInfoList) {
153             if (userInfo.isGuest()) {
154                 // Don't display guests in the switcher.
155                 continue;
156             }
157 
158             boolean isForeground = fgUserId == userInfo.id;
159             UserRecord record = new UserRecord(userInfo,
160                     isForeground ? UserRecord.FOREGROUND_USER : UserRecord.BACKGROUND_USER);
161             userRecords.add(record);
162         }
163 
164         // Add button for starting guest session.
165         userRecords.add(createStartGuestUserRecord());
166 
167         // Add add user record if the foreground user can add users
168         if (!mUserManager.hasUserRestriction(DISALLOW_ADD_USER, fgUserHandle)) {
169             userRecords.add(createAddUserRecord());
170         }
171 
172         return userRecords;
173     }
174 
createForegroundUserRecord()175     private UserRecord createForegroundUserRecord() {
176         return new UserRecord(mUserManager.getUserInfo(ActivityManager.getCurrentUser()),
177                 UserRecord.FOREGROUND_USER);
178     }
179 
180     /**
181      * Create guest user record
182      */
createStartGuestUserRecord()183     private UserRecord createStartGuestUserRecord() {
184         return new UserRecord(null /* userInfo */, UserRecord.START_GUEST);
185     }
186 
187     /**
188      * Create add user record
189      */
createAddUserRecord()190     private UserRecord createAddUserRecord() {
191         return new UserRecord(null /* userInfo */, UserRecord.ADD_USER);
192     }
193 
setUserSelectionListener(UserSelectionListener userSelectionListener)194     public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
195         mUserSelectionListener = userSelectionListener;
196     }
197 
198     /** Sets a {@link CarUserManager}. */
setCarUserManager(CarUserManager carUserManager)199     public void setCarUserManager(CarUserManager carUserManager) {
200         mCarUserManager = carUserManager;
201     }
202 
onUsersUpdate()203     private void onUsersUpdate() {
204         mAdapter.clearUsers();
205         mAdapter.updateUsers(createUserRecords(getUsersForUserGrid()));
206         mAdapter.notifyDataSetChanged();
207     }
208 
registerForUserEvents()209     private void registerForUserEvents() {
210         IntentFilter filter = new IntentFilter();
211         filter.addAction(Intent.ACTION_USER_REMOVED);
212         filter.addAction(Intent.ACTION_USER_ADDED);
213         filter.addAction(Intent.ACTION_USER_INFO_CHANGED);
214         filter.addAction(Intent.ACTION_USER_SWITCHED);
215         mContext.registerReceiverAsUser(
216                 mUserUpdateReceiver,
217                 UserHandle.ALL, // Necessary because CarSystemUi lives in User 0
218                 filter,
219                 /* broadcastPermission= */ null,
220                 /* scheduler= */ null);
221     }
222 
unregisterForUserEvents()223     private void unregisterForUserEvents() {
224         mContext.unregisterReceiver(mUserUpdateReceiver);
225     }
226 
227     /**
228      * Adapter to populate the grid layout with the available user profiles
229      */
230     public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder>
231             implements Dialog.OnClickListener, Dialog.OnCancelListener {
232 
233         private final Context mContext;
234         private List<UserRecord> mUsers;
235         private final Resources mRes;
236         private final String mGuestName;
237         private final String mNewUserName;
238         // View that holds the add user button.  Used to enable/disable the view
239         private View mAddUserView;
240         // User record for the add user.  Need to call notifyUserSelected only if the user
241         // confirms adding a user
242         private UserRecord mAddUserRecord;
243 
UserAdapter(Context context, List<UserRecord> users)244         public UserAdapter(Context context, List<UserRecord> users) {
245             mRes = context.getResources();
246             mContext = context;
247             updateUsers(users);
248             mGuestName = mRes.getString(R.string.car_guest);
249             mNewUserName = mRes.getString(R.string.car_new_user);
250         }
251 
252         /**
253          * Clears list of user records.
254          */
clearUsers()255         public void clearUsers() {
256             mUsers.clear();
257         }
258 
259         /**
260          * Updates list of user records.
261          */
updateUsers(List<UserRecord> users)262         public void updateUsers(List<UserRecord> users) {
263             mUsers = users;
264         }
265 
266         @Override
onCreateViewHolder(ViewGroup parent, int viewType)267         public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
268             View view = LayoutInflater.from(mContext)
269                     .inflate(R.layout.car_fullscreen_user_pod, parent, false);
270             view.setAlpha(1f);
271             view.bringToFront();
272             return new UserAdapterViewHolder(view);
273         }
274 
275         @Override
onBindViewHolder(UserAdapterViewHolder holder, int position)276         public void onBindViewHolder(UserAdapterViewHolder holder, int position) {
277             UserRecord userRecord = mUsers.get(position);
278 
279             Drawable circleIcon = getCircularUserRecordIcon(userRecord);
280 
281             if (userRecord.mInfo != null) {
282                 // User might have badges (like managed user)
283                 holder.mUserAvatarImageView.setDrawableWithBadge(circleIcon, userRecord.mInfo.id);
284             } else {
285                 // Guest or "Add User" don't have badges
286                 holder.mUserAvatarImageView.setDrawable(circleIcon);
287             }
288             holder.mUserNameTextView.setText(getUserRecordName(userRecord));
289 
290             holder.mView.setOnClickListener(v -> {
291                 if (userRecord == null) {
292                     return;
293                 }
294 
295                 switch (userRecord.mType) {
296                     case UserRecord.START_GUEST:
297                         notifyUserSelected(userRecord);
298                         UserInfo guest = createNewOrFindExistingGuest(mContext);
299                         if (guest != null) {
300                             if (!switchUser(guest.id)) {
301                                 Log.e(TAG, "Failed to switch to guest user: " + guest.id);
302                             }
303                         }
304                         break;
305                     case UserRecord.ADD_USER:
306                         // If the user wants to add a user, show dialog to confirm adding a user
307                         // Disable button so it cannot be clicked multiple times
308                         mAddUserView = holder.mView;
309                         mAddUserView.setEnabled(false);
310                         mAddUserRecord = userRecord;
311 
312                         handleAddUserClicked();
313                         break;
314                     default:
315                         // If the user doesn't want to be a guest or add a user, switch to the user
316                         // selected
317                         notifyUserSelected(userRecord);
318                         if (!switchUser(userRecord.mInfo.id)) {
319                             Log.e(TAG, "Failed to switch users: " + userRecord.mInfo.id);
320                         }
321                 }
322             });
323 
324         }
325 
handleAddUserClicked()326         private void handleAddUserClicked() {
327             if (!mUserManager.canAddMoreUsers()) {
328                 mAddUserView.setEnabled(true);
329                 showMaxUserLimitReachedDialog();
330             } else {
331                 showConfirmAddUserDialog();
332             }
333         }
334 
335         /**
336          * Get the maximum number of real (non-guest, non-managed profile) users that can be created
337          * on the device. This is a dynamic value and it decreases with the increase of the number
338          * of managed profiles on the device.
339          *
340          * <p> It excludes system user in headless system user model.
341          *
342          * @return Maximum number of real users that can be created.
343          */
getMaxSupportedRealUsers()344         private int getMaxSupportedRealUsers() {
345             int maxSupportedUsers = UserManager.getMaxSupportedUsers();
346             if (UserManager.isHeadlessSystemUserMode()) {
347                 maxSupportedUsers -= 1;
348             }
349 
350             List<UserInfo> users = mUserManager.getAliveUsers();
351 
352             // Count all users that are managed profiles of another user.
353             int managedProfilesCount = 0;
354             for (UserInfo user : users) {
355                 if (user.isManagedProfile()) {
356                     managedProfilesCount++;
357                 }
358             }
359 
360             return maxSupportedUsers - managedProfilesCount;
361         }
362 
showMaxUserLimitReachedDialog()363         private void showMaxUserLimitReachedDialog() {
364             AlertDialog maxUsersDialog = new Builder(mContext,
365                     com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
366                     .setTitle(R.string.profile_limit_reached_title)
367                     .setMessage(getResources().getQuantityString(
368                             R.plurals.profile_limit_reached_message,
369                             getMaxSupportedRealUsers(),
370                             getMaxSupportedRealUsers()))
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(R.string.start_guest_session);
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 userCreationResult.getUser();
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,
549                                 /* enable= */ true);
550                         UserHelper.assignDefaultIcon(mContext, user);
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