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