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