1 /* 2 * Copyright (C) 2018 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.car.settings.users; 18 19 import static android.os.UserManager.DISALLOW_ADD_USER; 20 import static android.os.UserManager.SWITCHABILITY_STATUS_OK; 21 22 import android.annotation.IntDef; 23 import android.app.Activity; 24 import android.app.ActivityManager; 25 import android.car.Car; 26 import android.car.user.CarUserManager; 27 import android.car.userlib.CarUserManagerHelper; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.pm.UserInfo; 33 import android.content.res.Resources; 34 import android.graphics.Rect; 35 import android.os.UserHandle; 36 import android.os.UserManager; 37 import android.util.AttributeSet; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.FrameLayout; 42 import android.widget.ImageView; 43 import android.widget.TextView; 44 45 import androidx.annotation.Nullable; 46 import androidx.core.graphics.drawable.RoundedBitmapDrawable; 47 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; 48 import androidx.recyclerview.widget.GridLayoutManager; 49 import androidx.recyclerview.widget.RecyclerView; 50 51 import com.android.car.settings.R; 52 import com.android.car.settings.common.BaseFragment; 53 import com.android.car.settings.common.ConfirmationDialogFragment; 54 import com.android.car.settings.common.ErrorDialog; 55 import com.android.internal.util.UserIcons; 56 57 import java.lang.annotation.Retention; 58 import java.lang.annotation.RetentionPolicy; 59 import java.util.ArrayList; 60 import java.util.List; 61 import java.util.stream.Collectors; 62 63 /** 64 * Displays a GridLayout with icons for the users in the system to allow switching between users. 65 * One of the uses of this is for the lock screen in auto. 66 */ 67 public class UserGridRecyclerView extends RecyclerView { 68 69 private static final String MAX_USERS_LIMIT_REACHED_DIALOG_TAG = 70 "com.android.car.settings.users.MaxUsersLimitReachedDialog"; 71 private static final String CONFIRM_CREATE_NEW_USER_DIALOG_TAG = 72 "com.android.car.settings.users.ConfirmCreateNewUserDialog"; 73 74 private UserAdapter mAdapter; 75 private CarUserManagerHelper mCarUserManagerHelper; 76 private UserManager mUserManager; 77 private Context mContext; 78 private BaseFragment mBaseFragment; 79 private AddNewUserTask mAddNewUserTask; 80 private boolean mEnableAddUserButton; 81 private UserIconProvider mUserIconProvider; 82 private Car mCar; 83 private CarUserManager mCarUserManager; 84 85 private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() { 86 @Override 87 public void onReceive(Context context, Intent intent) { 88 onUsersUpdate(); 89 } 90 }; 91 UserGridRecyclerView(Context context, AttributeSet attrs)92 public UserGridRecyclerView(Context context, AttributeSet attrs) { 93 super(context, attrs); 94 mContext = context; 95 mCarUserManagerHelper = new CarUserManagerHelper(mContext); 96 mUserManager = UserManager.get(mContext); 97 mUserIconProvider = new UserIconProvider(); 98 mEnableAddUserButton = true; 99 mCar = Car.createCar(mContext); 100 mCarUserManager = (CarUserManager) mCar.getCarManager(Car.CAR_USER_SERVICE); 101 102 addItemDecoration(new ItemSpacingDecoration(context.getResources().getDimensionPixelSize( 103 R.dimen.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 if (mAddNewUserTask != null) { 123 mAddNewUserTask.cancel(/* mayInterruptIfRunning= */ false); 124 } 125 if (mCar != null) { 126 mCar.disconnect(); 127 } 128 } 129 130 /** 131 * Initializes the adapter that populates the grid layout 132 */ buildAdapter()133 public void buildAdapter() { 134 List<UserRecord> userRecords = createUserRecords(getUsersForUserGrid()); 135 mAdapter = new UserAdapter(mContext, userRecords); 136 super.setAdapter(mAdapter); 137 } 138 createUserRecords(List<UserInfo> userInfoList)139 private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) { 140 int fgUserId = ActivityManager.getCurrentUser(); 141 UserHandle fgUserHandle = UserHandle.of(fgUserId); 142 List<UserRecord> userRecords = new ArrayList<>(); 143 144 // If the foreground user CANNOT switch to other users, only display the foreground user. 145 if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) { 146 userRecords.add(createForegroundUserRecord()); 147 return userRecords; 148 } 149 150 // If the foreground user CAN switch to other users, iterate through all users. 151 for (UserInfo userInfo : userInfoList) { 152 boolean isForeground = fgUserId == userInfo.id; 153 154 if (!isForeground && userInfo.isGuest()) { 155 // Don't display temporary running background guests in the switcher. 156 continue; 157 } 158 159 UserRecord record = new UserRecord(userInfo, 160 isForeground ? UserRecord.FOREGROUND_USER : UserRecord.BACKGROUND_USER); 161 userRecords.add(record); 162 } 163 164 // Add start guest user record if the system is not logged in as guest already. 165 if (!getCurrentForegroundUserInfo().isGuest()) { 166 userRecords.add(createStartGuestUserRecord()); 167 } 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(getCurrentForegroundUserInfo(), UserRecord.FOREGROUND_USER); 179 } 180 getCurrentForegroundUserInfo()181 private UserInfo getCurrentForegroundUserInfo() { 182 return mUserManager.getUserInfo(ActivityManager.getCurrentUser()); 183 } 184 185 /** 186 * Show the "Add User" Button 187 */ enableAddUser()188 public void enableAddUser() { 189 mEnableAddUserButton = true; 190 onUsersUpdate(); 191 } 192 193 /** 194 * Hide the "Add User" Button 195 */ disableAddUser()196 public void disableAddUser() { 197 mEnableAddUserButton = false; 198 onUsersUpdate(); 199 } 200 201 /** 202 * Create guest user record 203 */ createStartGuestUserRecord()204 private UserRecord createStartGuestUserRecord() { 205 return new UserRecord(/* userInfo= */ null, UserRecord.START_GUEST); 206 } 207 208 /** 209 * Create add user record 210 */ createAddUserRecord()211 private UserRecord createAddUserRecord() { 212 return new UserRecord(/* userInfo= */ null, UserRecord.ADD_USER); 213 } 214 setFragment(BaseFragment fragment)215 public void setFragment(BaseFragment fragment) { 216 mBaseFragment = fragment; 217 } 218 onUsersUpdate()219 private void onUsersUpdate() { 220 // If you can show the add user button, there is no restriction 221 mAdapter.setAddUserRestricted(!mEnableAddUserButton); 222 mAdapter.clearUsers(); 223 mAdapter.updateUsers(createUserRecords(getUsersForUserGrid())); 224 mAdapter.notifyDataSetChanged(); 225 } 226 getUsersForUserGrid()227 private List<UserInfo> getUsersForUserGrid() { 228 List<UserInfo> users = UserManager.get(mContext).getUsers(/* excludeDying= */ true); 229 return users.stream() 230 .filter(UserInfo::supportsSwitchToByUser) 231 .collect(Collectors.toList()); 232 } 233 registerForUserEvents()234 private void registerForUserEvents() { 235 IntentFilter filter = new IntentFilter(); 236 filter.addAction(Intent.ACTION_USER_REMOVED); 237 filter.addAction(Intent.ACTION_USER_ADDED); 238 filter.addAction(Intent.ACTION_USER_INFO_CHANGED); 239 filter.addAction(Intent.ACTION_USER_SWITCHED); 240 filter.addAction(Intent.ACTION_USER_STOPPED); 241 filter.addAction(Intent.ACTION_USER_UNLOCKED); 242 mContext.registerReceiverAsUser( 243 mUserUpdateReceiver, 244 UserHandle.ALL, 245 filter, 246 /* broadcastPermission= */ null, 247 /* scheduler= */ null); 248 } 249 unregisterForUserEvents()250 private void unregisterForUserEvents() { 251 mContext.unregisterReceiver(mUserUpdateReceiver); 252 } 253 254 /** 255 * Adapter to populate the grid layout with the available user profiles 256 */ 257 public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder> 258 implements AddNewUserTask.AddNewUserListener { 259 260 private final Context mContext; 261 private final Resources mRes; 262 private final String mGuestName; 263 264 private List<UserRecord> mUsers; 265 private String mNewUserName; 266 // View that holds the add user button. Used to enable/disable the view 267 private View mAddUserView; 268 private float mOpacityDisabled; 269 private float mOpacityEnabled; 270 private boolean mIsAddUserRestricted; 271 272 private final ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> { 273 mAddNewUserTask = new AddNewUserTask(mCarUserManagerHelper, 274 mCarUserManager, /* addNewUserListener= */this); 275 mAddNewUserTask.execute(mNewUserName); 276 }; 277 278 /** 279 * Enable the "add user" button if the user cancels adding an user 280 */ 281 private final ConfirmationDialogFragment.RejectListener mRejectListener = 282 arguments -> enableAddView(); 283 284 UserAdapter(Context context, List<UserRecord> users)285 public UserAdapter(Context context, List<UserRecord> users) { 286 mRes = context.getResources(); 287 mContext = context; 288 updateUsers(users); 289 mGuestName = mRes.getString(R.string.user_guest); 290 mNewUserName = mRes.getString(R.string.user_new_user_name); 291 mOpacityDisabled = mRes.getFloat(R.dimen.opacity_disabled); 292 mOpacityEnabled = mRes.getFloat(R.dimen.opacity_enabled); 293 resetDialogListeners(); 294 } 295 296 /** 297 * Removes all the users from the User Grid. 298 */ clearUsers()299 public void clearUsers() { 300 mUsers.clear(); 301 } 302 303 /** 304 * Refreshes the User Grid with the new List of users. 305 */ updateUsers(List<UserRecord> users)306 public void updateUsers(List<UserRecord> users) { 307 mUsers = users; 308 } 309 310 @Override onCreateViewHolder(ViewGroup parent, int viewType)311 public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 312 View view = LayoutInflater.from(mContext) 313 .inflate(R.layout.user_switcher_pod, parent, false); 314 view.setAlpha(mOpacityEnabled); 315 view.bringToFront(); 316 return new UserAdapterViewHolder(view); 317 } 318 319 @Override onBindViewHolder(UserAdapterViewHolder holder, int position)320 public void onBindViewHolder(UserAdapterViewHolder holder, int position) { 321 UserRecord userRecord = mUsers.get(position); 322 RoundedBitmapDrawable circleIcon = getCircularUserRecordIcon(userRecord); 323 holder.mUserAvatarImageView.setImageDrawable(circleIcon); 324 holder.mUserNameTextView.setText(getUserRecordName(userRecord)); 325 326 // Defaults to 100% opacity and no circle around the icon. 327 holder.mView.setAlpha(mOpacityEnabled); 328 holder.mFrame.setBackgroundResource(0); 329 330 // Foreground user record. 331 switch (userRecord.mType) { 332 case UserRecord.FOREGROUND_USER: 333 // Add a circle around the icon. 334 holder.mFrame.setBackgroundResource(R.drawable.user_avatar_bg_circle); 335 // Go back to quick settings if user selected is already the foreground user. 336 holder.mView.setOnClickListener(v 337 -> mBaseFragment.getActivity().onBackPressed()); 338 break; 339 340 case UserRecord.START_GUEST: 341 holder.mView.setOnClickListener(v -> handleGuestSessionClicked()); 342 break; 343 344 case UserRecord.ADD_USER: 345 if (mIsAddUserRestricted) { 346 // If there are restrictions, show a 50% opaque "add user" view 347 holder.mView.setAlpha(mOpacityDisabled); 348 holder.mView.setOnClickListener( 349 v -> mBaseFragment.getFragmentHost().showBlockingMessage()); 350 } else { 351 holder.mView.setOnClickListener(v -> handleAddUserClicked(v)); 352 } 353 break; 354 355 default: 356 // User record; 357 holder.mView.setOnClickListener(v -> handleUserSwitch(userRecord.mInfo)); 358 } 359 } 360 361 /** 362 * Specify if adding a user should be restricted. 363 * 364 * @param isAddUserRestricted should adding a user be restricted 365 */ setAddUserRestricted(boolean isAddUserRestricted)366 public void setAddUserRestricted(boolean isAddUserRestricted) { 367 mIsAddUserRestricted = isAddUserRestricted; 368 } 369 370 /** Resets listeners for shown dialog fragments. */ resetDialogListeners()371 private void resetDialogListeners() { 372 if (mBaseFragment != null) { 373 ConfirmationDialogFragment dialog = 374 (ConfirmationDialogFragment) mBaseFragment 375 .getFragmentManager() 376 .findFragmentByTag(CONFIRM_CREATE_NEW_USER_DIALOG_TAG); 377 ConfirmationDialogFragment.resetListeners( 378 dialog, 379 mConfirmListener, 380 mRejectListener, 381 /* neutralListener= */ null); 382 } 383 } 384 handleUserSwitch(UserInfo userInfo)385 private void handleUserSwitch(UserInfo userInfo) { 386 mCarUserManager.switchUser(userInfo.id).thenRun(() -> { 387 // Successful switch, close Settings app. 388 closeSettingsTask(); 389 }); 390 } 391 handleGuestSessionClicked()392 private void handleGuestSessionClicked() { 393 UserInfo guest = 394 UserHelper.getInstance(mContext).createNewOrFindExistingGuest(mContext); 395 if (guest != null) { 396 mCarUserManager.switchUser(guest.id).thenRun(() -> { 397 // Successful start, will switch to guest now. Close Settings app. 398 closeSettingsTask(); 399 }); 400 } 401 } 402 handleAddUserClicked(View addUserView)403 private void handleAddUserClicked(View addUserView) { 404 if (!mUserManager.canAddMoreUsers()) { 405 showMaxUsersLimitReachedDialog(); 406 } else { 407 mAddUserView = addUserView; 408 // Disable button so it cannot be clicked multiple times 409 mAddUserView.setEnabled(false); 410 showConfirmCreateNewUserDialog(); 411 } 412 } 413 showMaxUsersLimitReachedDialog()414 private void showMaxUsersLimitReachedDialog() { 415 ConfirmationDialogFragment dialogFragment = 416 UsersDialogProvider.getMaxUsersLimitReachedDialogFragment(getContext(), 417 UserHelper.getInstance(mContext).getMaxSupportedRealUsers()); 418 dialogFragment.show( 419 mBaseFragment.getFragmentManager(), MAX_USERS_LIMIT_REACHED_DIALOG_TAG); 420 } 421 showConfirmCreateNewUserDialog()422 private void showConfirmCreateNewUserDialog() { 423 ConfirmationDialogFragment dialogFragment = 424 UsersDialogProvider.getConfirmCreateNewUserDialogFragment(getContext(), 425 mConfirmListener, mRejectListener); 426 dialogFragment.show( 427 mBaseFragment.getFragmentManager(), CONFIRM_CREATE_NEW_USER_DIALOG_TAG); 428 } 429 getCircularUserRecordIcon(UserRecord userRecord)430 private RoundedBitmapDrawable getCircularUserRecordIcon(UserRecord userRecord) { 431 Resources resources = mContext.getResources(); 432 RoundedBitmapDrawable circleIcon; 433 switch (userRecord.mType) { 434 case UserRecord.START_GUEST: 435 circleIcon = mUserIconProvider.getRoundedGuestDefaultIcon(resources); 436 break; 437 case UserRecord.ADD_USER: 438 circleIcon = getCircularAddUserIcon(); 439 break; 440 default: 441 circleIcon = mUserIconProvider.getRoundedUserIcon(userRecord.mInfo, mContext); 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.user_add_circle))); 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(R.string.start_guest_session); 459 break; 460 case UserRecord.ADD_USER: 461 recordName = mContext.getString(R.string.user_add_user_menu); 462 break; 463 default: 464 recordName = userRecord.mInfo.name; 465 } 466 return recordName; 467 } 468 469 @Override onUserAddedSuccess()470 public void onUserAddedSuccess() { 471 enableAddView(); 472 // New user added. Will switch to new user, therefore close the app. 473 closeSettingsTask(); 474 } 475 476 @Override onUserAddedFailure()477 public void onUserAddedFailure() { 478 enableAddView(); 479 // Display failure dialog. 480 if (mBaseFragment != null) { 481 ErrorDialog.show(mBaseFragment, R.string.add_user_error_title); 482 } 483 } 484 485 /** 486 * When we switch users, we also want to finish the QuickSettingActivity, so we send back a 487 * result telling the QuickSettingActivity to finish. 488 */ closeSettingsTask()489 private void closeSettingsTask() { 490 mBaseFragment.getActivity().setResult(Activity.FINISH_TASK_WITH_ACTIVITY, new Intent()); 491 mBaseFragment.getActivity().finish(); 492 } 493 494 @Override getItemCount()495 public int getItemCount() { 496 return mUsers.size(); 497 } 498 499 /** 500 * Layout for each individual pod in the Grid RecyclerView 501 */ 502 public class UserAdapterViewHolder extends RecyclerView.ViewHolder { 503 504 public ImageView mUserAvatarImageView; 505 public TextView mUserNameTextView; 506 public View mView; 507 public FrameLayout mFrame; 508 UserAdapterViewHolder(View view)509 public UserAdapterViewHolder(View view) { 510 super(view); 511 mView = view; 512 mUserAvatarImageView = view.findViewById(R.id.user_avatar); 513 mUserNameTextView = view.findViewById(R.id.user_name); 514 mFrame = view.findViewById(R.id.current_user_frame); 515 } 516 } 517 enableAddView()518 private void enableAddView() { 519 if (mAddUserView != null) { 520 mAddUserView.setEnabled(true); 521 } 522 } 523 } 524 525 /** 526 * Object wrapper class for the userInfo. Use it to distinguish if a profile is a 527 * guest profile, add user profile, or the foreground user. 528 */ 529 public static final class UserRecord { 530 531 public final UserInfo mInfo; 532 public final @UserRecordType int mType; 533 534 public static final int START_GUEST = 0; 535 public static final int ADD_USER = 1; 536 public static final int FOREGROUND_USER = 2; 537 public static final int BACKGROUND_USER = 3; 538 539 @IntDef({START_GUEST, ADD_USER, FOREGROUND_USER, BACKGROUND_USER}) 540 @Retention(RetentionPolicy.SOURCE) 541 public @interface UserRecordType {} 542 UserRecord(@ullable UserInfo userInfo, @UserRecordType int recordType)543 public UserRecord(@Nullable UserInfo userInfo, @UserRecordType int recordType) { 544 mInfo = userInfo; 545 mType = recordType; 546 } 547 } 548 549 /** 550 * A {@link RecyclerView.ItemDecoration} that will add spacing between each item in the 551 * RecyclerView that it is added to. 552 */ 553 private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration { 554 private int mItemSpacing; 555 ItemSpacingDecoration(int itemSpacing)556 private ItemSpacingDecoration(int itemSpacing) { 557 mItemSpacing = itemSpacing; 558 } 559 560 @Override getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)561 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 562 RecyclerView.State state) { 563 super.getItemOffsets(outRect, view, parent, state); 564 int position = parent.getChildAdapterPosition(view); 565 566 // Skip offset for last item except for GridLayoutManager. 567 if (position == state.getItemCount() - 1 568 && !(parent.getLayoutManager() instanceof GridLayoutManager)) { 569 return; 570 } 571 572 outRect.bottom = mItemSpacing; 573 } 574 } 575 } 576