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