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.profiles; 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.graphics.drawable.Drawable; 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.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.admin.ui.UserAvatarView; 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 profiles in the system to allow switching between 65 * profiles. One of the uses of this is for the lock screen in auto. 66 */ 67 public class ProfileGridRecyclerView extends RecyclerView { 68 69 private static final String MAX_PROFILES_LIMIT_REACHED_DIALOG_TAG = 70 "com.android.car.settings.profiles.MaxProfilesLimitReachedDialog"; 71 private static final String CONFIRM_CREATE_NEW_PROFILE_DIALOG_TAG = 72 "com.android.car.settings.profiles.ConfirmCreateNewProfileDialog"; 73 74 private ProfileAdapter mAdapter; 75 private UserManager mUserManager; 76 private Context mContext; 77 private BaseFragment mBaseFragment; 78 private AddNewProfileTask mAddNewProfileTask; 79 private boolean mEnableAddProfileButton; 80 private ProfileIconProvider mProfileIconProvider; 81 private Car mCar; 82 private CarUserManager mCarUserManager; 83 84 private final BroadcastReceiver mProfileUpdateReceiver = new BroadcastReceiver() { 85 @Override 86 public void onReceive(Context context, Intent intent) { 87 onProfilesUpdate(); 88 } 89 }; 90 ProfileGridRecyclerView(Context context, AttributeSet attrs)91 public ProfileGridRecyclerView(Context context, AttributeSet attrs) { 92 super(context, attrs); 93 mContext = context; 94 mUserManager = UserManager.get(mContext); 95 mProfileIconProvider = new ProfileIconProvider(); 96 mEnableAddProfileButton = true; 97 mCar = Car.createCar(mContext); 98 mCarUserManager = (CarUserManager) mCar.getCarManager(Car.CAR_USER_SERVICE); 99 100 addItemDecoration(new ItemSpacingDecoration(context.getResources().getDimensionPixelSize( 101 R.dimen.profile_switcher_vertical_spacing_between_profiles))); 102 } 103 104 /** 105 * Register listener for any update to the profiles 106 */ 107 @Override onFinishInflate()108 public void onFinishInflate() { 109 super.onFinishInflate(); 110 registerForProfileEvents(); 111 } 112 113 /** 114 * Unregisters listener checking for any change to the profiles 115 */ 116 @Override onDetachedFromWindow()117 public void onDetachedFromWindow() { 118 super.onDetachedFromWindow(); 119 unregisterForProfileEvents(); 120 if (mAddNewProfileTask != null) { 121 mAddNewProfileTask.cancel(/* mayInterruptIfRunning= */ false); 122 } 123 if (mCar != null) { 124 mCar.disconnect(); 125 } 126 } 127 128 /** 129 * Initializes the adapter that populates the grid layout 130 */ buildAdapter()131 public void buildAdapter() { 132 List<ProfileRecord> profileRecords = createProfileRecords(getProfilesForProfileGrid()); 133 mAdapter = new ProfileAdapter(mContext, profileRecords); 134 super.setAdapter(mAdapter); 135 } 136 createProfileRecords(List<UserInfo> userInfoList)137 private List<ProfileRecord> createProfileRecords(List<UserInfo> userInfoList) { 138 int fgUserId = ActivityManager.getCurrentUser(); 139 UserHandle fgUserHandle = UserHandle.of(fgUserId); 140 List<ProfileRecord> profileRecords = new ArrayList<>(); 141 142 // If the foreground profile CANNOT switch to other profiles, only display the foreground 143 // profile. 144 if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) { 145 profileRecords.add(createForegroundProfileRecord()); 146 return profileRecords; 147 } 148 149 // If the foreground profile CAN switch to other profiles, iterate through all profiles. 150 for (UserInfo userInfo : userInfoList) { 151 boolean isForeground = fgUserId == userInfo.id; 152 153 if (!isForeground && userInfo.isGuest()) { 154 // Don't display temporary running background guests in the switcher. 155 continue; 156 } 157 158 ProfileRecord record = new ProfileRecord(userInfo, isForeground 159 ? ProfileRecord.FOREGROUND_PROFILE : ProfileRecord.BACKGROUND_PROFILE); 160 profileRecords.add(record); 161 } 162 163 // Add start guest profile record if the system is not logged in as guest already. 164 if (!getCurrentForegroundProfileInfo().isGuest()) { 165 profileRecords.add(createStartGuestProfileRecord()); 166 } 167 168 // Add "add profile" record if the foreground profile can add profiles 169 if (!mUserManager.hasUserRestriction(DISALLOW_ADD_USER, fgUserHandle)) { 170 profileRecords.add(createAddProfileRecord()); 171 } 172 173 return profileRecords; 174 } 175 createForegroundProfileRecord()176 private ProfileRecord createForegroundProfileRecord() { 177 return new ProfileRecord(getCurrentForegroundProfileInfo(), 178 ProfileRecord.FOREGROUND_PROFILE); 179 } 180 getCurrentForegroundProfileInfo()181 private UserInfo getCurrentForegroundProfileInfo() { 182 return mUserManager.getUserInfo(ActivityManager.getCurrentUser()); 183 } 184 185 /** 186 * Show the "Add Profile" Button 187 */ enableAddProfile()188 public void enableAddProfile() { 189 mEnableAddProfileButton = true; 190 onProfilesUpdate(); 191 } 192 193 /** 194 * Hide the "Add Profile" Button 195 */ disableAddProfile()196 public void disableAddProfile() { 197 mEnableAddProfileButton = false; 198 onProfilesUpdate(); 199 } 200 201 /** 202 * Create guest profile record 203 */ createStartGuestProfileRecord()204 private ProfileRecord createStartGuestProfileRecord() { 205 return new ProfileRecord(/* profileInfo= */ null, ProfileRecord.START_GUEST); 206 } 207 208 /** 209 * Create add profile record 210 */ createAddProfileRecord()211 private ProfileRecord createAddProfileRecord() { 212 return new ProfileRecord(/* profileInfo= */ null, ProfileRecord.ADD_PROFILE); 213 } 214 setFragment(BaseFragment fragment)215 public void setFragment(BaseFragment fragment) { 216 mBaseFragment = fragment; 217 } 218 onProfilesUpdate()219 private void onProfilesUpdate() { 220 // If you can show the add profile button, there is no restriction 221 mAdapter.setAddProfileRestricted(!mEnableAddProfileButton); 222 mAdapter.clearProfiles(); 223 mAdapter.updateProfiles(createProfileRecords(getProfilesForProfileGrid())); 224 mAdapter.notifyDataSetChanged(); 225 } 226 getProfilesForProfileGrid()227 private List<UserInfo> getProfilesForProfileGrid() { 228 List<UserInfo> users = UserManager.get(mContext).getAliveUsers(); 229 return users.stream() 230 .filter(userInfo -> userInfo.supportsSwitchTo() && userInfo.isFull()) 231 .sorted((u1, u2) -> Long.signum(u1.creationTime - u2.creationTime)) 232 .collect(Collectors.toList()); 233 } 234 registerForProfileEvents()235 private void registerForProfileEvents() { 236 IntentFilter filter = new IntentFilter(); 237 filter.addAction(Intent.ACTION_USER_REMOVED); 238 filter.addAction(Intent.ACTION_USER_ADDED); 239 filter.addAction(Intent.ACTION_USER_INFO_CHANGED); 240 filter.addAction(Intent.ACTION_USER_SWITCHED); 241 filter.addAction(Intent.ACTION_USER_STOPPED); 242 filter.addAction(Intent.ACTION_USER_UNLOCKED); 243 mContext.registerReceiverAsUser( 244 mProfileUpdateReceiver, 245 UserHandle.ALL, 246 filter, 247 /* broadcastPermission= */ null, 248 /* scheduler= */ null); 249 } 250 unregisterForProfileEvents()251 private void unregisterForProfileEvents() { 252 mContext.unregisterReceiver(mProfileUpdateReceiver); 253 } 254 255 /** 256 * Adapter to populate the grid layout with the available user profiles 257 */ 258 public final class ProfileAdapter extends 259 RecyclerView.Adapter<ProfileAdapter.ProfileAdapterViewHolder> 260 implements AddNewProfileTask.AddNewProfileListener { 261 262 private final Resources mRes; 263 private final String mGuestName; 264 265 private Context mContext; 266 private List<ProfileRecord> mProfiles; 267 private String mNewProfileName; 268 // View that holds the add profile button. Used to enable/disable the view 269 private View mAddProfileView; 270 private float mOpacityDisabled; 271 private float mOpacityEnabled; 272 private boolean mIsAddProfileRestricted; 273 274 private final ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> { 275 mAddNewProfileTask = new AddNewProfileTask(mContext, 276 mCarUserManager, /* addNewProfileListener= */this); 277 mAddNewProfileTask.execute(mNewProfileName); 278 }; 279 280 /** 281 * Enable the "add profile" button if the user cancels adding a profile 282 */ 283 private final ConfirmationDialogFragment.RejectListener mRejectListener = 284 arguments -> enableAddView(); 285 286 ProfileAdapter(Context context, List<ProfileRecord> profiles)287 public ProfileAdapter(Context context, List<ProfileRecord> profiles) { 288 mRes = context.getResources(); 289 mContext = context; 290 updateProfiles(profiles); 291 mGuestName = mRes.getString(com.android.internal.R.string.guest_name); 292 mNewProfileName = mRes.getString(R.string.user_new_user_name); 293 mOpacityDisabled = mRes.getFloat(R.dimen.opacity_disabled); 294 mOpacityEnabled = mRes.getFloat(R.dimen.opacity_enabled); 295 resetDialogListeners(); 296 } 297 298 /** 299 * Removes all the profiles from the Profile Grid. 300 */ clearProfiles()301 public void clearProfiles() { 302 mProfiles.clear(); 303 } 304 305 /** 306 * Refreshes the Profile Grid with the new List of profiles. 307 */ updateProfiles(List<ProfileRecord> profiles)308 public void updateProfiles(List<ProfileRecord> profiles) { 309 mProfiles = profiles; 310 } 311 312 @Override onCreateViewHolder(ViewGroup parent, int viewType)313 public ProfileAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 314 View view = LayoutInflater.from(mContext) 315 .inflate(R.layout.profile_switcher_pod, parent, false); 316 view.setAlpha(mOpacityEnabled); 317 view.bringToFront(); 318 return new ProfileAdapterViewHolder(view); 319 } 320 321 @Override onBindViewHolder(ProfileAdapterViewHolder holder, int position)322 public void onBindViewHolder(ProfileAdapterViewHolder holder, int position) { 323 ProfileRecord profileRecord = mProfiles.get(position); 324 Drawable circleIcon = getCircularProfileRecordIcon(profileRecord); 325 if (profileRecord.mType == ProfileRecord.ADD_PROFILE) { 326 // 'Add Profile' has badges if device admin exists. 327 holder.mProfileAvatarImageView.setDrawableWithBadge(circleIcon); 328 } else if (profileRecord.mInfo != null) { 329 // Profile might have badges (like managed profile) 330 holder.mProfileAvatarImageView.setDrawableWithBadge(circleIcon, 331 profileRecord.mInfo.id); 332 } else { 333 // Guest does not have badges 334 holder.mProfileAvatarImageView.setDrawable(circleIcon); 335 } 336 holder.mProfileNameTextView.setText(getProfileRecordName(profileRecord)); 337 338 // Defaults to 100% opacity and no circle around the icon. 339 holder.mView.setAlpha(mOpacityEnabled); 340 holder.mFrame.setBackgroundResource(0); 341 342 // Foreground profile record. 343 switch (profileRecord.mType) { 344 case ProfileRecord.FOREGROUND_PROFILE: 345 // Add a circle around the icon. 346 holder.mFrame.setBackgroundResource(R.drawable.profile_avatar_bg_circle); 347 // Go back to quick settings if profile selected is already the foreground 348 // profile. 349 holder.mView.setOnClickListener(v 350 -> mBaseFragment.getActivity().onBackPressed()); 351 break; 352 353 case ProfileRecord.START_GUEST: 354 holder.mView.setOnClickListener(v -> handleGuestSessionClicked()); 355 break; 356 357 case ProfileRecord.ADD_PROFILE: 358 if (mIsAddProfileRestricted) { 359 // If there are restrictions, show a 50% opaque "add profile" view 360 holder.mView.setAlpha(mOpacityDisabled); 361 holder.mView.setOnClickListener( 362 v -> mBaseFragment.getFragmentHost().showBlockingMessage()); 363 } else { 364 holder.mView.setOnClickListener(v -> handleAddProfileClicked(v)); 365 } 366 break; 367 368 default: 369 // Profile record; 370 holder.mView.setOnClickListener(v -> handleProfileSwitch(profileRecord.mInfo)); 371 } 372 } 373 374 /** 375 * Specify if adding a profile should be restricted. 376 * 377 * @param isAddProfileRestricted should adding a profile be restricted 378 */ setAddProfileRestricted(boolean isAddProfileRestricted)379 public void setAddProfileRestricted(boolean isAddProfileRestricted) { 380 mIsAddProfileRestricted = isAddProfileRestricted; 381 } 382 383 /** Resets listeners for shown dialog fragments. */ resetDialogListeners()384 private void resetDialogListeners() { 385 if (mBaseFragment != null) { 386 ConfirmationDialogFragment dialog = 387 (ConfirmationDialogFragment) mBaseFragment 388 .getFragmentManager() 389 .findFragmentByTag(CONFIRM_CREATE_NEW_PROFILE_DIALOG_TAG); 390 ConfirmationDialogFragment.resetListeners( 391 dialog, 392 mConfirmListener, 393 mRejectListener, 394 /* neutralListener= */ null); 395 } 396 } 397 handleProfileSwitch(UserInfo userInfo)398 private void handleProfileSwitch(UserInfo userInfo) { 399 mCarUserManager.switchUser(userInfo.id).whenCompleteAsync((r, e) -> { 400 // Successful switch, close Settings app. 401 closeSettingsTask(); 402 }, Runnable::run); 403 } 404 handleGuestSessionClicked()405 private void handleGuestSessionClicked() { 406 UserInfo guest = 407 ProfileHelper.getInstance(mContext).createNewOrFindExistingGuest(mContext); 408 if (guest != null) { 409 mCarUserManager.switchUser(guest.id).whenCompleteAsync((r, e) -> { 410 // Successful start, will switch to guest now. Close Settings app. 411 closeSettingsTask(); 412 }, Runnable::run); 413 } 414 } 415 handleAddProfileClicked(View addProfileView)416 private void handleAddProfileClicked(View addProfileView) { 417 if (!mUserManager.canAddMoreUsers()) { 418 showMaxProfilesLimitReachedDialog(); 419 } else { 420 mAddProfileView = addProfileView; 421 // Disable button so it cannot be clicked multiple times 422 mAddProfileView.setEnabled(false); 423 showConfirmCreateNewProfileDialog(); 424 } 425 } 426 showMaxProfilesLimitReachedDialog()427 private void showMaxProfilesLimitReachedDialog() { 428 ConfirmationDialogFragment dialogFragment = 429 ProfilesDialogProvider.getMaxProfilesLimitReachedDialogFragment(getContext(), 430 ProfileHelper.getInstance(mContext).getMaxSupportedRealProfiles()); 431 dialogFragment.show( 432 mBaseFragment.getFragmentManager(), MAX_PROFILES_LIMIT_REACHED_DIALOG_TAG); 433 } 434 showConfirmCreateNewProfileDialog()435 private void showConfirmCreateNewProfileDialog() { 436 ConfirmationDialogFragment dialogFragment = 437 ProfilesDialogProvider.getConfirmCreateNewProfileDialogFragment(getContext(), 438 mConfirmListener, mRejectListener); 439 dialogFragment.show( 440 mBaseFragment.getFragmentManager(), CONFIRM_CREATE_NEW_PROFILE_DIALOG_TAG); 441 } 442 getCircularProfileRecordIcon(ProfileRecord profileRecord)443 private Drawable getCircularProfileRecordIcon(ProfileRecord profileRecord) { 444 Drawable circleIcon; 445 switch (profileRecord.mType) { 446 case ProfileRecord.START_GUEST: 447 circleIcon = mProfileIconProvider.getRoundedGuestDefaultIcon(mContext); 448 break; 449 case ProfileRecord.ADD_PROFILE: 450 circleIcon = getCircularAddProfileIcon(); 451 break; 452 default: 453 circleIcon = mProfileIconProvider.getRoundedProfileIcon(profileRecord.mInfo, 454 mContext); 455 } 456 return circleIcon; 457 } 458 getCircularAddProfileIcon()459 private RoundedBitmapDrawable getCircularAddProfileIcon() { 460 RoundedBitmapDrawable circleIcon = 461 RoundedBitmapDrawableFactory.create(mRes, UserIcons.convertToBitmap( 462 mContext.getDrawable(R.drawable.profile_add_circle))); 463 circleIcon.setCircular(true); 464 return circleIcon; 465 } 466 getProfileRecordName(ProfileRecord profileRecord)467 private String getProfileRecordName(ProfileRecord profileRecord) { 468 String recordName; 469 switch (profileRecord.mType) { 470 case ProfileRecord.START_GUEST: 471 recordName = mContext.getString(com.android.internal.R.string.guest_name); 472 break; 473 case ProfileRecord.ADD_PROFILE: 474 recordName = mContext.getString(R.string.user_add_user_menu); 475 break; 476 default: 477 recordName = profileRecord.mInfo.name; 478 } 479 return recordName; 480 } 481 482 @Override onProfileAddedSuccess()483 public void onProfileAddedSuccess() { 484 enableAddView(); 485 // New profile added. Will switch to new profile, therefore close the app. 486 closeSettingsTask(); 487 } 488 489 @Override onProfileAddedFailure()490 public void onProfileAddedFailure() { 491 enableAddView(); 492 // Display failure dialog. 493 if (mBaseFragment != null) { 494 ErrorDialog.show(mBaseFragment, R.string.add_user_error_title); 495 } 496 } 497 498 /** 499 * When we switch profiles, we also want to finish the QuickSettingActivity, so we send back 500 * a result telling the QuickSettingActivity to finish. 501 */ closeSettingsTask()502 private void closeSettingsTask() { 503 mBaseFragment.getActivity().setResult(Activity.FINISH_TASK_WITH_ACTIVITY, new Intent()); 504 mBaseFragment.getActivity().finish(); 505 } 506 507 @Override getItemCount()508 public int getItemCount() { 509 return mProfiles.size(); 510 } 511 512 /** 513 * Layout for each individual pod in the Grid RecyclerView 514 */ 515 public class ProfileAdapterViewHolder extends RecyclerView.ViewHolder { 516 517 public UserAvatarView mProfileAvatarImageView; 518 public TextView mProfileNameTextView; 519 public View mView; 520 public FrameLayout mFrame; 521 ProfileAdapterViewHolder(View view)522 public ProfileAdapterViewHolder(View view) { 523 super(view); 524 mView = view; 525 mProfileAvatarImageView = view.findViewById(R.id.profile_avatar); 526 mProfileNameTextView = view.findViewById(R.id.profile_name); 527 mFrame = view.findViewById(R.id.current_profile_frame); 528 } 529 } 530 enableAddView()531 private void enableAddView() { 532 if (mAddProfileView != null) { 533 mAddProfileView.setEnabled(true); 534 } 535 } 536 } 537 538 /** 539 * Object wrapper class for the userInfo. Use it to distinguish if a profile is a 540 * guest profile, add user profile, or the foreground profile. 541 */ 542 public static final class ProfileRecord { 543 544 public final UserInfo mInfo; 545 public final @ProfileRecordType int mType; 546 547 public static final int START_GUEST = 0; 548 public static final int ADD_PROFILE = 1; 549 public static final int FOREGROUND_PROFILE = 2; 550 public static final int BACKGROUND_PROFILE = 3; 551 552 @IntDef({START_GUEST, ADD_PROFILE, FOREGROUND_PROFILE, BACKGROUND_PROFILE}) 553 @Retention(RetentionPolicy.SOURCE) 554 public @interface ProfileRecordType {} 555 ProfileRecord(@ullable UserInfo userInfo, @ProfileRecordType int recordType)556 public ProfileRecord(@Nullable UserInfo userInfo, @ProfileRecordType int recordType) { 557 mInfo = userInfo; 558 mType = recordType; 559 } 560 } 561 562 /** 563 * A {@link RecyclerView.ItemDecoration} that will add spacing between each item in the 564 * RecyclerView that it is added to. 565 */ 566 private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration { 567 private int mItemSpacing; 568 ItemSpacingDecoration(int itemSpacing)569 private ItemSpacingDecoration(int itemSpacing) { 570 mItemSpacing = itemSpacing; 571 } 572 573 @Override getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)574 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 575 RecyclerView.State state) { 576 super.getItemOffsets(outRect, view, parent, state); 577 int position = parent.getChildAdapterPosition(view); 578 579 // Skip offset for last item except for GridLayoutManager. 580 if (position == state.getItemCount() - 1 581 && !(parent.getLayoutManager() instanceof GridLayoutManager)) { 582 return; 583 } 584 585 outRect.bottom = mItemSpacing; 586 } 587 } 588 } 589