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