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