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.systemui.statusbar.car; 18 19 import static android.content.DialogInterface.BUTTON_NEGATIVE; 20 import static android.content.DialogInterface.BUTTON_POSITIVE; 21 22 import android.app.AlertDialog; 23 import android.app.AlertDialog.Builder; 24 import android.app.Dialog; 25 import android.car.userlib.CarUserManagerHelper; 26 import android.content.Context; 27 import android.content.DialogInterface; 28 import android.content.pm.UserInfo; 29 import android.content.res.Resources; 30 import android.graphics.Bitmap; 31 import android.graphics.Rect; 32 import android.os.AsyncTask; 33 import android.util.AttributeSet; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.ImageView; 38 import android.widget.TextView; 39 40 import androidx.core.graphics.drawable.RoundedBitmapDrawable; 41 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; 42 import androidx.recyclerview.widget.GridLayoutManager; 43 import androidx.recyclerview.widget.RecyclerView; 44 45 import com.android.internal.util.UserIcons; 46 import com.android.systemui.R; 47 import com.android.systemui.statusbar.phone.SystemUIDialog; 48 49 import java.util.ArrayList; 50 import java.util.List; 51 52 /** 53 * Displays a GridLayout with icons for the users in the system to allow switching between users. 54 * One of the uses of this is for the lock screen in auto. 55 */ 56 public class UserGridRecyclerView extends RecyclerView implements 57 CarUserManagerHelper.OnUsersUpdateListener { 58 private UserSelectionListener mUserSelectionListener; 59 private UserAdapter mAdapter; 60 private CarUserManagerHelper mCarUserManagerHelper; 61 private Context mContext; 62 UserGridRecyclerView(Context context, AttributeSet attrs)63 public UserGridRecyclerView(Context context, AttributeSet attrs) { 64 super(context, attrs); 65 mContext = context; 66 mCarUserManagerHelper = new CarUserManagerHelper(mContext); 67 68 addItemDecoration(new ItemSpacingDecoration(context.getResources().getDimensionPixelSize( 69 R.dimen.car_user_switcher_vertical_spacing_between_users))); 70 } 71 72 /** 73 * Register listener for any update to the users 74 */ 75 @Override onFinishInflate()76 public void onFinishInflate() { 77 super.onFinishInflate(); 78 mCarUserManagerHelper.registerOnUsersUpdateListener(this); 79 } 80 81 /** 82 * Unregisters listener checking for any change to the users 83 */ 84 @Override onDetachedFromWindow()85 public void onDetachedFromWindow() { 86 super.onDetachedFromWindow(); 87 mCarUserManagerHelper.unregisterOnUsersUpdateListener(this); 88 } 89 90 /** 91 * Initializes the adapter that populates the grid layout 92 * 93 * @return the adapter 94 */ buildAdapter()95 public void buildAdapter() { 96 List<UserRecord> userRecords = createUserRecords(mCarUserManagerHelper 97 .getAllUsers()); 98 mAdapter = new UserAdapter(mContext, userRecords); 99 super.setAdapter(mAdapter); 100 } 101 createUserRecords(List<UserInfo> userInfoList)102 private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) { 103 List<UserRecord> userRecords = new ArrayList<>(); 104 105 // If the foreground user CANNOT switch to other users, only display the foreground user. 106 if (!mCarUserManagerHelper.canForegroundUserSwitchUsers()) { 107 userRecords.add(createForegroundUserRecord()); 108 return userRecords; 109 } 110 111 for (UserInfo userInfo : userInfoList) { 112 if (userInfo.isGuest()) { 113 // Don't display guests in the switcher. 114 continue; 115 } 116 117 boolean isForeground = 118 mCarUserManagerHelper.getCurrentForegroundUserId() == userInfo.id; 119 UserRecord record = new UserRecord(userInfo, false /* isStartGuestSession */, 120 false /* isAddUser */, isForeground); 121 userRecords.add(record); 122 } 123 124 // Add button for starting guest session. 125 userRecords.add(createStartGuestUserRecord()); 126 127 // Add add user record if the foreground user can add users 128 if (mCarUserManagerHelper.canForegroundUserAddUsers()) { 129 userRecords.add(createAddUserRecord()); 130 } 131 132 return userRecords; 133 } 134 createForegroundUserRecord()135 private UserRecord createForegroundUserRecord() { 136 return new UserRecord(mCarUserManagerHelper.getCurrentForegroundUserInfo(), 137 false /* isStartGuestSession */, false /* isAddUser */, true /* isForeground */); 138 } 139 140 /** 141 * Create guest user record 142 */ createStartGuestUserRecord()143 private UserRecord createStartGuestUserRecord() { 144 UserInfo userInfo = new UserInfo(); 145 userInfo.name = mContext.getString(R.string.start_guest_session); 146 return new UserRecord(userInfo, true /* isStartGuestSession */, false /* isAddUser */, 147 false /* isForeground */); 148 } 149 150 /** 151 * Create add user record 152 */ createAddUserRecord()153 private UserRecord createAddUserRecord() { 154 UserInfo userInfo = new UserInfo(); 155 userInfo.name = mContext.getString(R.string.car_add_user); 156 return new UserRecord(userInfo, false /* isStartGuestSession */, 157 true /* isAddUser */, false /* isForeground */); 158 } 159 setUserSelectionListener(UserSelectionListener userSelectionListener)160 public void setUserSelectionListener(UserSelectionListener userSelectionListener) { 161 mUserSelectionListener = userSelectionListener; 162 } 163 164 @Override onUsersUpdate()165 public void onUsersUpdate() { 166 mAdapter.clearUsers(); 167 mAdapter.updateUsers(createUserRecords(mCarUserManagerHelper.getAllUsers())); 168 mAdapter.notifyDataSetChanged(); 169 } 170 171 /** 172 * Adapter to populate the grid layout with the available user profiles 173 */ 174 public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder> 175 implements Dialog.OnClickListener, Dialog.OnCancelListener { 176 177 private final Context mContext; 178 private List<UserRecord> mUsers; 179 private final Resources mRes; 180 private final String mGuestName; 181 private final String mNewUserName; 182 // View that holds the add user button. Used to enable/disable the view 183 private View mAddUserView; 184 // User record for the add user. Need to call notifyUserSelected only if the user 185 // confirms adding a user 186 private UserRecord mAddUserRecord; 187 UserAdapter(Context context, List<UserRecord> users)188 public UserAdapter(Context context, List<UserRecord> users) { 189 mRes = context.getResources(); 190 mContext = context; 191 updateUsers(users); 192 mGuestName = mRes.getString(R.string.car_guest); 193 mNewUserName = mRes.getString(R.string.car_new_user); 194 } 195 clearUsers()196 public void clearUsers() { 197 mUsers.clear(); 198 } 199 updateUsers(List<UserRecord> users)200 public void updateUsers(List<UserRecord> users) { 201 mUsers = users; 202 } 203 204 @Override onCreateViewHolder(ViewGroup parent, int viewType)205 public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 206 View view = LayoutInflater.from(mContext) 207 .inflate(R.layout.car_fullscreen_user_pod, parent, false); 208 view.setAlpha(1f); 209 view.bringToFront(); 210 return new UserAdapterViewHolder(view); 211 } 212 213 @Override onBindViewHolder(UserAdapterViewHolder holder, int position)214 public void onBindViewHolder(UserAdapterViewHolder holder, int position) { 215 UserRecord userRecord = mUsers.get(position); 216 RoundedBitmapDrawable circleIcon = RoundedBitmapDrawableFactory.create(mRes, 217 getUserRecordIcon(userRecord)); 218 circleIcon.setCircular(true); 219 holder.mUserAvatarImageView.setImageDrawable(circleIcon); 220 holder.mUserNameTextView.setText(userRecord.mInfo.name); 221 222 holder.mView.setOnClickListener(v -> { 223 if (userRecord == null) { 224 return; 225 } 226 227 if (userRecord.mIsStartGuestSession) { 228 notifyUserSelected(userRecord); 229 mCarUserManagerHelper.startGuestSession(mGuestName); 230 return; 231 } 232 233 // If the user wants to add a user, show dialog to confirm adding a user 234 if (userRecord.mIsAddUser) { 235 // Disable button so it cannot be clicked multiple times 236 mAddUserView = holder.mView; 237 mAddUserView.setEnabled(false); 238 mAddUserRecord = userRecord; 239 240 handleAddUserClicked(); 241 return; 242 } 243 // If the user doesn't want to be a guest or add a user, switch to the user selected 244 notifyUserSelected(userRecord); 245 mCarUserManagerHelper.switchToUser(userRecord.mInfo); 246 }); 247 248 } 249 handleAddUserClicked()250 private void handleAddUserClicked() { 251 if (mCarUserManagerHelper.isUserLimitReached()) { 252 mAddUserView.setEnabled(true); 253 showMaxUserLimitReachedDialog(); 254 } else { 255 showConfirmAddUserDialog(); 256 } 257 } 258 showMaxUserLimitReachedDialog()259 private void showMaxUserLimitReachedDialog() { 260 AlertDialog maxUsersDialog = new Builder(mContext, 261 com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert) 262 .setTitle(R.string.user_limit_reached_title) 263 .setMessage(getResources().getQuantityString( 264 R.plurals.user_limit_reached_message, 265 mCarUserManagerHelper.getMaxSupportedRealUsers(), 266 mCarUserManagerHelper.getMaxSupportedRealUsers())) 267 .setPositiveButton(android.R.string.ok, null) 268 .create(); 269 // Sets window flags for the SysUI dialog 270 SystemUIDialog.applyFlags(maxUsersDialog); 271 maxUsersDialog.show(); 272 } 273 showConfirmAddUserDialog()274 private void showConfirmAddUserDialog() { 275 String message = mRes.getString(R.string.user_add_user_message_setup) 276 .concat(System.getProperty("line.separator")) 277 .concat(System.getProperty("line.separator")) 278 .concat(mRes.getString(R.string.user_add_user_message_update)); 279 280 AlertDialog addUserDialog = new Builder(mContext, 281 com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert) 282 .setTitle(R.string.user_add_user_title) 283 .setMessage(message) 284 .setNegativeButton(android.R.string.cancel, this) 285 .setPositiveButton(android.R.string.ok, this) 286 .setOnCancelListener(this) 287 .create(); 288 // Sets window flags for the SysUI dialog 289 SystemUIDialog.applyFlags(addUserDialog); 290 addUserDialog.show(); 291 } 292 notifyUserSelected(UserRecord userRecord)293 private void notifyUserSelected(UserRecord userRecord) { 294 // Notify the listener which user was selected 295 if (mUserSelectionListener != null) { 296 mUserSelectionListener.onUserSelected(userRecord); 297 } 298 } 299 getUserRecordIcon(UserRecord userRecord)300 private Bitmap getUserRecordIcon(UserRecord userRecord) { 301 if (userRecord.mIsStartGuestSession) { 302 return mCarUserManagerHelper.getGuestDefaultIcon(); 303 } 304 305 if (userRecord.mIsAddUser) { 306 return UserIcons.convertToBitmap(mContext 307 .getDrawable(R.drawable.car_add_circle_round)); 308 } 309 310 return mCarUserManagerHelper.getUserIcon(userRecord.mInfo); 311 } 312 313 @Override onClick(DialogInterface dialog, int which)314 public void onClick(DialogInterface dialog, int which) { 315 if (which == BUTTON_POSITIVE) { 316 notifyUserSelected(mAddUserRecord); 317 new AddNewUserTask().execute(mNewUserName); 318 } else if (which == BUTTON_NEGATIVE) { 319 // Enable the add button only if cancel 320 if (mAddUserView != null) { 321 mAddUserView.setEnabled(true); 322 } 323 } 324 } 325 326 @Override onCancel(DialogInterface dialog)327 public void onCancel(DialogInterface dialog) { 328 // Enable the add button again if user cancels dialog by clicking outside the dialog 329 if (mAddUserView != null) { 330 mAddUserView.setEnabled(true); 331 } 332 } 333 334 private class AddNewUserTask extends AsyncTask<String, Void, UserInfo> { 335 336 @Override doInBackground(String... userNames)337 protected UserInfo doInBackground(String... userNames) { 338 return mCarUserManagerHelper.createNewNonAdminUser(userNames[0]); 339 } 340 341 @Override onPreExecute()342 protected void onPreExecute() { 343 } 344 345 @Override onPostExecute(UserInfo user)346 protected void onPostExecute(UserInfo user) { 347 if (user != null) { 348 mCarUserManagerHelper.switchToUser(user); 349 } 350 } 351 } 352 353 @Override getItemCount()354 public int getItemCount() { 355 return mUsers.size(); 356 } 357 358 public class UserAdapterViewHolder extends RecyclerView.ViewHolder { 359 360 public ImageView mUserAvatarImageView; 361 public TextView mUserNameTextView; 362 public View mView; 363 UserAdapterViewHolder(View view)364 public UserAdapterViewHolder(View view) { 365 super(view); 366 mView = view; 367 mUserAvatarImageView = (ImageView) view.findViewById(R.id.user_avatar); 368 mUserNameTextView = (TextView) view.findViewById(R.id.user_name); 369 } 370 } 371 } 372 373 /** 374 * Object wrapper class for the userInfo. Use it to distinguish if a profile is a 375 * guest profile, add user profile, or the foreground user. 376 */ 377 public static final class UserRecord { 378 379 public final UserInfo mInfo; 380 public final boolean mIsStartGuestSession; 381 public final boolean mIsAddUser; 382 public final boolean mIsForeground; 383 UserRecord(UserInfo userInfo, boolean isStartGuestSession, boolean isAddUser, boolean isForeground)384 public UserRecord(UserInfo userInfo, boolean isStartGuestSession, boolean isAddUser, 385 boolean isForeground) { 386 mInfo = userInfo; 387 mIsStartGuestSession = isStartGuestSession; 388 mIsAddUser = isAddUser; 389 mIsForeground = isForeground; 390 } 391 } 392 393 /** 394 * Listener used to notify when a user has been selected 395 */ 396 interface UserSelectionListener { 397 onUserSelected(UserRecord record)398 void onUserSelected(UserRecord record); 399 } 400 401 /** 402 * A {@link RecyclerView.ItemDecoration} that will add spacing between each item in the 403 * RecyclerView that it is added to. 404 */ 405 private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration { 406 private int mItemSpacing; 407 ItemSpacingDecoration(int itemSpacing)408 private ItemSpacingDecoration(int itemSpacing) { 409 mItemSpacing = itemSpacing; 410 } 411 412 @Override getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)413 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 414 RecyclerView.State state) { 415 super.getItemOffsets(outRect, view, parent, state); 416 int position = parent.getChildAdapterPosition(view); 417 418 // Skip offset for last item except for GridLayoutManager. 419 if (position == state.getItemCount() - 1 420 && !(parent.getLayoutManager() instanceof GridLayoutManager)) { 421 return; 422 } 423 424 outRect.bottom = mItemSpacing; 425 } 426 } 427 } 428