• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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