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