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.util.concurrent.AsyncFuture; 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.graphics.drawable.Drawable; 45 import android.os.AsyncTask; 46 import android.os.UserHandle; 47 import android.os.UserManager; 48 import android.sysprop.CarProperties; 49 import android.util.AttributeSet; 50 import android.util.Log; 51 import android.view.LayoutInflater; 52 import android.view.View; 53 import android.view.ViewGroup; 54 import android.view.Window; 55 import android.view.WindowManager; 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.car.admin.ui.UserAvatarView; 64 import com.android.car.internal.user.UserHelper; 65 import com.android.internal.util.UserIcons; 66 import com.android.settingslib.utils.StringUtil; 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 .sorted((u1, u2) -> Long.signum(u1.creationTime - u2.creationTime)) 140 .collect(Collectors.toList()); 141 } 142 createUserRecords(List<UserInfo> userInfoList)143 private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) { 144 int fgUserId = ActivityManager.getCurrentUser(); 145 UserHandle fgUserHandle = UserHandle.of(fgUserId); 146 List<UserRecord> userRecords = new ArrayList<>(); 147 148 // If the foreground user CANNOT switch to other users, only display the foreground user. 149 if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) { 150 userRecords.add(createForegroundUserRecord()); 151 return userRecords; 152 } 153 154 for (UserInfo userInfo : userInfoList) { 155 if (userInfo.isGuest()) { 156 // Don't display guests in the switcher. 157 continue; 158 } 159 160 boolean isForeground = fgUserId == userInfo.id; 161 UserRecord record = new UserRecord(userInfo, 162 isForeground ? UserRecord.FOREGROUND_USER : UserRecord.BACKGROUND_USER); 163 userRecords.add(record); 164 } 165 166 // Add button for starting guest session. 167 userRecords.add(createStartGuestUserRecord()); 168 169 // Add add user record if the foreground user can add users 170 if (!mUserManager.hasUserRestriction(DISALLOW_ADD_USER, fgUserHandle)) { 171 userRecords.add(createAddUserRecord()); 172 } 173 174 return userRecords; 175 } 176 createForegroundUserRecord()177 private UserRecord createForegroundUserRecord() { 178 return new UserRecord(mUserManager.getUserInfo(ActivityManager.getCurrentUser()), 179 UserRecord.FOREGROUND_USER); 180 } 181 182 /** 183 * Create guest user record 184 */ createStartGuestUserRecord()185 private UserRecord createStartGuestUserRecord() { 186 return new UserRecord(null /* userInfo */, UserRecord.START_GUEST); 187 } 188 189 /** 190 * Create add user record 191 */ createAddUserRecord()192 private UserRecord createAddUserRecord() { 193 return new UserRecord(null /* userInfo */, UserRecord.ADD_USER); 194 } 195 setUserSelectionListener(UserSelectionListener userSelectionListener)196 public void setUserSelectionListener(UserSelectionListener userSelectionListener) { 197 mUserSelectionListener = userSelectionListener; 198 } 199 200 /** Sets a {@link CarUserManager}. */ setCarUserManager(CarUserManager carUserManager)201 public void setCarUserManager(CarUserManager carUserManager) { 202 mCarUserManager = carUserManager; 203 } 204 onUsersUpdate()205 private void onUsersUpdate() { 206 mAdapter.clearUsers(); 207 mAdapter.updateUsers(createUserRecords(getUsersForUserGrid())); 208 mAdapter.notifyDataSetChanged(); 209 } 210 registerForUserEvents()211 private void registerForUserEvents() { 212 IntentFilter filter = new IntentFilter(); 213 filter.addAction(Intent.ACTION_USER_REMOVED); 214 filter.addAction(Intent.ACTION_USER_ADDED); 215 filter.addAction(Intent.ACTION_USER_INFO_CHANGED); 216 filter.addAction(Intent.ACTION_USER_SWITCHED); 217 mContext.registerReceiverAsUser( 218 mUserUpdateReceiver, 219 UserHandle.ALL, // Necessary because CarSystemUi lives in User 0 220 filter, 221 /* broadcastPermission= */ null, 222 /* scheduler= */ null); 223 } 224 unregisterForUserEvents()225 private void unregisterForUserEvents() { 226 mContext.unregisterReceiver(mUserUpdateReceiver); 227 } 228 229 /** 230 * Adapter to populate the grid layout with the available user profiles 231 */ 232 public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder> 233 implements Dialog.OnClickListener, Dialog.OnCancelListener { 234 235 private final Context mContext; 236 private List<UserRecord> mUsers; 237 private final Resources mRes; 238 private final String mGuestName; 239 private final String mNewUserName; 240 // View that holds the add user button. Used to enable/disable the view 241 private View mAddUserView; 242 // User record for the add user. Need to call notifyUserSelected only if the user 243 // confirms adding a user 244 private UserRecord mAddUserRecord; 245 UserAdapter(Context context, List<UserRecord> users)246 public UserAdapter(Context context, List<UserRecord> users) { 247 mRes = context.getResources(); 248 mContext = context; 249 updateUsers(users); 250 mGuestName = mRes.getString(com.android.internal.R.string.guest_name); 251 mNewUserName = mRes.getString(R.string.car_new_user); 252 } 253 254 /** 255 * Clears list of user records. 256 */ clearUsers()257 public void clearUsers() { 258 mUsers.clear(); 259 } 260 261 /** 262 * Updates list of user records. 263 */ updateUsers(List<UserRecord> users)264 public void updateUsers(List<UserRecord> users) { 265 mUsers = users; 266 } 267 268 @Override onCreateViewHolder(ViewGroup parent, int viewType)269 public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 270 View view = LayoutInflater.from(mContext) 271 .inflate(R.layout.car_fullscreen_user_pod, parent, false); 272 view.setAlpha(1f); 273 view.bringToFront(); 274 return new UserAdapterViewHolder(view); 275 } 276 277 @Override onBindViewHolder(UserAdapterViewHolder holder, int position)278 public void onBindViewHolder(UserAdapterViewHolder holder, int position) { 279 UserRecord userRecord = mUsers.get(position); 280 281 Drawable circleIcon = getCircularUserRecordIcon(userRecord); 282 283 if (userRecord.mInfo != null) { 284 // User might have badges (like managed user) 285 holder.mUserAvatarImageView.setDrawableWithBadge(circleIcon, userRecord.mInfo.id); 286 } else { 287 // Guest or "Add User" don't have badges 288 holder.mUserAvatarImageView.setDrawable(circleIcon); 289 } 290 holder.mUserNameTextView.setText(getUserRecordName(userRecord)); 291 292 holder.mView.setOnClickListener(v -> { 293 if (userRecord == null) { 294 return; 295 } 296 297 switch (userRecord.mType) { 298 case UserRecord.START_GUEST: 299 notifyUserSelected(userRecord); 300 UserInfo guest = createNewOrFindExistingGuest(mContext); 301 if (guest != null) { 302 if (!switchUser(guest.id)) { 303 Log.e(TAG, "Failed to switch to guest user: " + guest.id); 304 } 305 } 306 break; 307 case UserRecord.ADD_USER: 308 // If the user wants to add a user, show dialog to confirm adding a user 309 // Disable button so it cannot be clicked multiple times 310 mAddUserView = holder.mView; 311 mAddUserView.setEnabled(false); 312 mAddUserRecord = userRecord; 313 314 handleAddUserClicked(); 315 break; 316 default: 317 // If the user doesn't want to be a guest or add a user, switch to the user 318 // selected 319 notifyUserSelected(userRecord); 320 if (!switchUser(userRecord.mInfo.id)) { 321 Log.e(TAG, "Failed to switch users: " + userRecord.mInfo.id); 322 } 323 } 324 }); 325 326 } 327 handleAddUserClicked()328 private void handleAddUserClicked() { 329 if (!mUserManager.canAddMoreUsers()) { 330 mAddUserView.setEnabled(true); 331 showMaxUserLimitReachedDialog(); 332 } else { 333 showConfirmAddUserDialog(); 334 } 335 } 336 337 /** 338 * Get the maximum number of real (non-guest, non-managed profile) users that can be created 339 * on the device. This is a dynamic value and it decreases with the increase of the number 340 * of managed profiles on the device. 341 * 342 * <p> It excludes system user in headless system user model. 343 * 344 * @return Maximum number of real users that can be created. 345 */ getMaxSupportedRealUsers()346 private int getMaxSupportedRealUsers() { 347 int maxSupportedUsers = UserManager.getMaxSupportedUsers(); 348 if (UserManager.isHeadlessSystemUserMode()) { 349 maxSupportedUsers -= 1; 350 } 351 352 List<UserInfo> users = mUserManager.getAliveUsers(); 353 354 // Count all users that are managed profiles of another user. 355 int managedProfilesCount = 0; 356 for (UserInfo user : users) { 357 if (user.isManagedProfile()) { 358 managedProfilesCount++; 359 } 360 } 361 362 return maxSupportedUsers - managedProfilesCount; 363 } 364 showMaxUserLimitReachedDialog()365 private void showMaxUserLimitReachedDialog() { 366 AlertDialog maxUsersDialog = new Builder(mContext, 367 com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert) 368 .setTitle(R.string.profile_limit_reached_title) 369 .setMessage(StringUtil.getIcuPluralsString(mContext, getMaxSupportedRealUsers(), 370 R.string.profile_limit_reached_message)) 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(com.android.internal.R.string.guest_name); 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 mUserManager.getUserInfo(userCreationResult.getUser().getIdentifier()); 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.getUserHandle(), 549 /* enable= */ true); 550 UserHelper.assignDefaultIcon(mContext, user.getUserHandle()); 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