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