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