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