| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.statusbar.car; |
| |
| import static android.content.DialogInterface.BUTTON_NEGATIVE; |
| import static android.content.DialogInterface.BUTTON_POSITIVE; |
| import static android.os.UserManager.DISALLOW_ADD_USER; |
| import static android.os.UserManager.SWITCHABILITY_STATUS_OK; |
| |
| import android.annotation.IntDef; |
| import android.annotation.Nullable; |
| import android.app.ActivityManager; |
| import android.app.AlertDialog; |
| import android.app.AlertDialog.Builder; |
| import android.app.Dialog; |
| import android.car.userlib.CarUserManagerHelper; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.UserInfo; |
| import android.content.res.Resources; |
| import android.graphics.Rect; |
| import android.os.AsyncTask; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.util.AttributeSet; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import androidx.core.graphics.drawable.RoundedBitmapDrawable; |
| import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; |
| import androidx.recyclerview.widget.GridLayoutManager; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.internal.util.UserIcons; |
| import com.android.systemui.R; |
| import com.android.systemui.statusbar.phone.SystemUIDialog; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Displays a GridLayout with icons for the users in the system to allow switching between users. |
| * One of the uses of this is for the lock screen in auto. |
| */ |
| public class UserGridRecyclerView extends RecyclerView { |
| private UserSelectionListener mUserSelectionListener; |
| private UserAdapter mAdapter; |
| private CarUserManagerHelper mCarUserManagerHelper; |
| private UserManager mUserManager; |
| private Context mContext; |
| private UserIconProvider mUserIconProvider; |
| |
| private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| onUsersUpdate(); |
| } |
| }; |
| |
| public UserGridRecyclerView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| mContext = context; |
| mCarUserManagerHelper = new CarUserManagerHelper(mContext); |
| mUserManager = UserManager.get(mContext); |
| mUserIconProvider = new UserIconProvider(); |
| |
| addItemDecoration(new ItemSpacingDecoration(mContext.getResources().getDimensionPixelSize( |
| R.dimen.car_user_switcher_vertical_spacing_between_users))); |
| } |
| |
| /** |
| * Register listener for any update to the users |
| */ |
| @Override |
| public void onFinishInflate() { |
| super.onFinishInflate(); |
| registerForUserEvents(); |
| } |
| |
| /** |
| * Unregisters listener checking for any change to the users |
| */ |
| @Override |
| public void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| unregisterForUserEvents(); |
| } |
| |
| /** |
| * Initializes the adapter that populates the grid layout |
| * |
| * @return the adapter |
| */ |
| public void buildAdapter() { |
| List<UserRecord> userRecords = createUserRecords(getUsersForUserGrid()); |
| mAdapter = new UserAdapter(mContext, userRecords); |
| super.setAdapter(mAdapter); |
| } |
| |
| private List<UserInfo> getUsersForUserGrid() { |
| return mUserManager.getUsers(/* excludeDying= */ true) |
| .stream() |
| .filter(UserInfo::supportsSwitchToByUser) |
| .collect(Collectors.toList()); |
| } |
| |
| private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) { |
| int fgUserId = ActivityManager.getCurrentUser(); |
| UserHandle fgUserHandle = UserHandle.of(fgUserId); |
| List<UserRecord> userRecords = new ArrayList<>(); |
| |
| // If the foreground user CANNOT switch to other users, only display the foreground user. |
| if (mUserManager.getUserSwitchability(fgUserHandle) != SWITCHABILITY_STATUS_OK) { |
| userRecords.add(createForegroundUserRecord()); |
| return userRecords; |
| } |
| |
| for (UserInfo userInfo : userInfoList) { |
| if (userInfo.isGuest()) { |
| // Don't display guests in the switcher. |
| continue; |
| } |
| |
| boolean isForeground = fgUserId == userInfo.id; |
| UserRecord record = new UserRecord(userInfo, |
| isForeground ? UserRecord.FOREGROUND_USER : UserRecord.BACKGROUND_USER); |
| userRecords.add(record); |
| } |
| |
| // Add button for starting guest session. |
| userRecords.add(createStartGuestUserRecord()); |
| |
| // Add add user record if the foreground user can add users |
| if (!mUserManager.hasUserRestriction(DISALLOW_ADD_USER, fgUserHandle)) { |
| userRecords.add(createAddUserRecord()); |
| } |
| |
| return userRecords; |
| } |
| |
| private UserRecord createForegroundUserRecord() { |
| return new UserRecord(mUserManager.getUserInfo(ActivityManager.getCurrentUser()), |
| UserRecord.FOREGROUND_USER); |
| } |
| |
| /** |
| * Create guest user record |
| */ |
| private UserRecord createStartGuestUserRecord() { |
| return new UserRecord(null /* userInfo */, UserRecord.START_GUEST); |
| } |
| |
| /** |
| * Create add user record |
| */ |
| private UserRecord createAddUserRecord() { |
| return new UserRecord(null /* userInfo */, UserRecord.ADD_USER); |
| } |
| |
| public void setUserSelectionListener(UserSelectionListener userSelectionListener) { |
| mUserSelectionListener = userSelectionListener; |
| } |
| |
| private void onUsersUpdate() { |
| mAdapter.clearUsers(); |
| mAdapter.updateUsers(createUserRecords(getUsersForUserGrid())); |
| mAdapter.notifyDataSetChanged(); |
| } |
| |
| private void registerForUserEvents() { |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(Intent.ACTION_USER_REMOVED); |
| filter.addAction(Intent.ACTION_USER_ADDED); |
| filter.addAction(Intent.ACTION_USER_INFO_CHANGED); |
| filter.addAction(Intent.ACTION_USER_SWITCHED); |
| mContext.registerReceiverAsUser( |
| mUserUpdateReceiver, |
| UserHandle.ALL, // Necessary because CarSystemUi lives in User 0 |
| filter, |
| /* broadcastPermission= */ null, |
| /* scheduler= */ null); |
| } |
| |
| private void unregisterForUserEvents() { |
| mContext.unregisterReceiver(mUserUpdateReceiver); |
| } |
| |
| /** |
| * Adapter to populate the grid layout with the available user profiles |
| */ |
| public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder> |
| implements Dialog.OnClickListener, Dialog.OnCancelListener { |
| |
| private final Context mContext; |
| private List<UserRecord> mUsers; |
| private final Resources mRes; |
| private final String mGuestName; |
| private final String mNewUserName; |
| // View that holds the add user button. Used to enable/disable the view |
| private View mAddUserView; |
| // User record for the add user. Need to call notifyUserSelected only if the user |
| // confirms adding a user |
| private UserRecord mAddUserRecord; |
| |
| public UserAdapter(Context context, List<UserRecord> users) { |
| mRes = context.getResources(); |
| mContext = context; |
| updateUsers(users); |
| mGuestName = mRes.getString(R.string.car_guest); |
| mNewUserName = mRes.getString(R.string.car_new_user); |
| } |
| |
| public void clearUsers() { |
| mUsers.clear(); |
| } |
| |
| public void updateUsers(List<UserRecord> users) { |
| mUsers = users; |
| } |
| |
| @Override |
| public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
| View view = LayoutInflater.from(mContext) |
| .inflate(R.layout.car_fullscreen_user_pod, parent, false); |
| view.setAlpha(1f); |
| view.bringToFront(); |
| return new UserAdapterViewHolder(view); |
| } |
| |
| @Override |
| public void onBindViewHolder(UserAdapterViewHolder holder, int position) { |
| UserRecord userRecord = mUsers.get(position); |
| RoundedBitmapDrawable circleIcon = getCircularUserRecordIcon(userRecord); |
| holder.mUserAvatarImageView.setImageDrawable(circleIcon); |
| holder.mUserNameTextView.setText(getUserRecordName(userRecord)); |
| |
| holder.mView.setOnClickListener(v -> { |
| if (userRecord == null) { |
| return; |
| } |
| |
| switch (userRecord.mType) { |
| case UserRecord.START_GUEST: |
| notifyUserSelected(userRecord); |
| UserInfo guest = createNewOrFindExistingGuest(mContext); |
| if (guest != null) { |
| mCarUserManagerHelper.switchToUser(guest); |
| } |
| break; |
| case UserRecord.ADD_USER: |
| // If the user wants to add a user, show dialog to confirm adding a user |
| // Disable button so it cannot be clicked multiple times |
| mAddUserView = holder.mView; |
| mAddUserView.setEnabled(false); |
| mAddUserRecord = userRecord; |
| |
| handleAddUserClicked(); |
| break; |
| default: |
| // If the user doesn't want to be a guest or add a user, switch to the user |
| // selected |
| notifyUserSelected(userRecord); |
| mCarUserManagerHelper.switchToUser(userRecord.mInfo); |
| } |
| }); |
| |
| } |
| |
| private void handleAddUserClicked() { |
| if (!mUserManager.canAddMoreUsers()) { |
| mAddUserView.setEnabled(true); |
| showMaxUserLimitReachedDialog(); |
| } else { |
| showConfirmAddUserDialog(); |
| } |
| } |
| |
| /** |
| * Get the maximum number of real (non-guest, non-managed profile) users that can be created |
| * on the device. This is a dynamic value and it decreases with the increase of the number |
| * of managed profiles on the device. |
| * |
| * <p> It excludes system user in headless system user model. |
| * |
| * @return Maximum number of real users that can be created. |
| */ |
| private int getMaxSupportedRealUsers() { |
| int maxSupportedUsers = UserManager.getMaxSupportedUsers(); |
| if (UserManager.isHeadlessSystemUserMode()) { |
| maxSupportedUsers -= 1; |
| } |
| |
| List<UserInfo> users = mUserManager.getUsers(/* excludeDying= */ true); |
| |
| // Count all users that are managed profiles of another user. |
| int managedProfilesCount = 0; |
| for (UserInfo user : users) { |
| if (user.isManagedProfile()) { |
| managedProfilesCount++; |
| } |
| } |
| |
| return maxSupportedUsers - managedProfilesCount; |
| } |
| |
| private void showMaxUserLimitReachedDialog() { |
| AlertDialog maxUsersDialog = new Builder(mContext, |
| com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert) |
| .setTitle(R.string.user_limit_reached_title) |
| .setMessage(getResources().getQuantityString( |
| R.plurals.user_limit_reached_message, |
| getMaxSupportedRealUsers(), |
| getMaxSupportedRealUsers())) |
| .setPositiveButton(android.R.string.ok, null) |
| .create(); |
| // Sets window flags for the SysUI dialog |
| SystemUIDialog.applyFlags(maxUsersDialog); |
| maxUsersDialog.show(); |
| } |
| |
| private void showConfirmAddUserDialog() { |
| String message = mRes.getString(R.string.user_add_user_message_setup) |
| .concat(System.getProperty("line.separator")) |
| .concat(System.getProperty("line.separator")) |
| .concat(mRes.getString(R.string.user_add_user_message_update)); |
| |
| AlertDialog addUserDialog = new Builder(mContext, |
| com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert) |
| .setTitle(R.string.user_add_user_title) |
| .setMessage(message) |
| .setNegativeButton(android.R.string.cancel, this) |
| .setPositiveButton(android.R.string.ok, this) |
| .setOnCancelListener(this) |
| .create(); |
| // Sets window flags for the SysUI dialog |
| SystemUIDialog.applyFlags(addUserDialog); |
| addUserDialog.show(); |
| } |
| |
| private void notifyUserSelected(UserRecord userRecord) { |
| // Notify the listener which user was selected |
| if (mUserSelectionListener != null) { |
| mUserSelectionListener.onUserSelected(userRecord); |
| } |
| } |
| |
| private RoundedBitmapDrawable getCircularUserRecordIcon(UserRecord userRecord) { |
| Resources resources = mContext.getResources(); |
| RoundedBitmapDrawable circleIcon; |
| switch (userRecord.mType) { |
| case UserRecord.START_GUEST: |
| circleIcon = mUserIconProvider.getRoundedGuestDefaultIcon(resources); |
| break; |
| case UserRecord.ADD_USER: |
| circleIcon = getCircularAddUserIcon(); |
| break; |
| default: |
| circleIcon = mUserIconProvider.getRoundedUserIcon(userRecord.mInfo, mContext); |
| break; |
| } |
| return circleIcon; |
| } |
| |
| private RoundedBitmapDrawable getCircularAddUserIcon() { |
| RoundedBitmapDrawable circleIcon = |
| RoundedBitmapDrawableFactory.create(mRes, UserIcons.convertToBitmap( |
| mContext.getDrawable(R.drawable.car_add_circle_round))); |
| circleIcon.setCircular(true); |
| return circleIcon; |
| } |
| |
| private String getUserRecordName(UserRecord userRecord) { |
| String recordName; |
| switch (userRecord.mType) { |
| case UserRecord.START_GUEST: |
| recordName = mContext.getString(R.string.start_guest_session); |
| break; |
| case UserRecord.ADD_USER: |
| recordName = mContext.getString(R.string.car_add_user); |
| break; |
| default: |
| recordName = userRecord.mInfo.name; |
| break; |
| } |
| return recordName; |
| } |
| |
| /** |
| * Finds the existing Guest user, or creates one if it doesn't exist. |
| * @param context App context |
| * @return UserInfo representing the Guest user |
| */ |
| @Nullable |
| public UserInfo createNewOrFindExistingGuest(Context context) { |
| // CreateGuest will return null if a guest already exists. |
| UserInfo newGuest = mUserManager.createGuest(context, mGuestName); |
| if (newGuest != null) { |
| new UserIconProvider().assignDefaultIcon( |
| mUserManager, context.getResources(), newGuest); |
| return newGuest; |
| } |
| |
| return mUserManager.findCurrentGuestUser(); |
| } |
| |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| if (which == BUTTON_POSITIVE) { |
| notifyUserSelected(mAddUserRecord); |
| new AddNewUserTask().execute(mNewUserName); |
| } else if (which == BUTTON_NEGATIVE) { |
| // Enable the add button only if cancel |
| if (mAddUserView != null) { |
| mAddUserView.setEnabled(true); |
| } |
| } |
| } |
| |
| @Override |
| public void onCancel(DialogInterface dialog) { |
| // Enable the add button again if user cancels dialog by clicking outside the dialog |
| if (mAddUserView != null) { |
| mAddUserView.setEnabled(true); |
| } |
| } |
| |
| private class AddNewUserTask extends AsyncTask<String, Void, UserInfo> { |
| |
| @Override |
| protected UserInfo doInBackground(String... userNames) { |
| return mCarUserManagerHelper.createNewNonAdminUser(userNames[0]); |
| } |
| |
| @Override |
| protected void onPreExecute() { |
| } |
| |
| @Override |
| protected void onPostExecute(UserInfo user) { |
| if (user != null) { |
| mCarUserManagerHelper.switchToUser(user); |
| } |
| } |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mUsers.size(); |
| } |
| |
| public class UserAdapterViewHolder extends RecyclerView.ViewHolder { |
| |
| public ImageView mUserAvatarImageView; |
| public TextView mUserNameTextView; |
| public View mView; |
| |
| public UserAdapterViewHolder(View view) { |
| super(view); |
| mView = view; |
| mUserAvatarImageView = (ImageView) view.findViewById(R.id.user_avatar); |
| mUserNameTextView = (TextView) view.findViewById(R.id.user_name); |
| } |
| } |
| } |
| |
| /** |
| * Object wrapper class for the userInfo. Use it to distinguish if a profile is a |
| * guest profile, add user profile, or the foreground user. |
| */ |
| public static final class UserRecord { |
| public final UserInfo mInfo; |
| public final @UserRecordType int mType; |
| |
| public static final int START_GUEST = 0; |
| public static final int ADD_USER = 1; |
| public static final int FOREGROUND_USER = 2; |
| public static final int BACKGROUND_USER = 3; |
| |
| @IntDef({START_GUEST, ADD_USER, FOREGROUND_USER, BACKGROUND_USER}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface UserRecordType{} |
| |
| public UserRecord(@Nullable UserInfo userInfo, @UserRecordType int recordType) { |
| mInfo = userInfo; |
| mType = recordType; |
| } |
| } |
| |
| /** |
| * Listener used to notify when a user has been selected |
| */ |
| interface UserSelectionListener { |
| |
| void onUserSelected(UserRecord record); |
| } |
| |
| /** |
| * A {@link RecyclerView.ItemDecoration} that will add spacing between each item in the |
| * RecyclerView that it is added to. |
| */ |
| private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration { |
| private int mItemSpacing; |
| |
| private ItemSpacingDecoration(int itemSpacing) { |
| mItemSpacing = itemSpacing; |
| } |
| |
| @Override |
| public void getItemOffsets(Rect outRect, View view, RecyclerView parent, |
| RecyclerView.State state) { |
| super.getItemOffsets(outRect, view, parent, state); |
| int position = parent.getChildAdapterPosition(view); |
| |
| // Skip offset for last item except for GridLayoutManager. |
| if (position == state.getItemCount() - 1 |
| && !(parent.getLayoutManager() instanceof GridLayoutManager)) { |
| return; |
| } |
| |
| outRect.bottom = mItemSpacing; |
| } |
| } |
| } |