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