1 /*
2  * Copyright (C) 2023 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.settingslib.users;
18 
19 import android.annotation.IntDef;
20 import android.app.Activity;
21 import android.app.Dialog;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.SharedPreferences;
25 import android.graphics.Bitmap;
26 import android.graphics.drawable.Drawable;
27 import android.os.Bundle;
28 import android.os.UserHandle;
29 import android.os.UserManager;
30 import android.view.View;
31 import android.widget.EditText;
32 import android.widget.ImageView;
33 import android.widget.RadioButton;
34 import android.widget.RadioGroup;
35 
36 import androidx.annotation.VisibleForTesting;
37 
38 import com.android.internal.util.UserIcons;
39 import com.android.settingslib.R;
40 import com.android.settingslib.RestrictedLockUtils;
41 import com.android.settingslib.RestrictedLockUtilsInternal;
42 import com.android.settingslib.drawable.CircleFramedDrawable;
43 import com.android.settingslib.utils.CustomDialogHelper;
44 import com.android.settingslib.utils.ThreadUtils;
45 
46 import java.io.File;
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 
50 /**
51  * This class encapsulates a Dialog for editing the user nickname and photo.
52  */
53 public class CreateUserDialogController {
54 
55     private static final String KEY_AWAITING_RESULT = "awaiting_result";
56     private static final String KEY_CURRENT_STATE = "current_state";
57     private static final String KEY_SAVED_PHOTO = "pending_photo";
58     private static final String KEY_SAVED_NAME = "saved_name";
59     private static final String KEY_IS_ADMIN = "admin_status";
60     private static final String KEY_ADD_USER_LONG_MESSAGE_DISPLAYED =
61             "key_add_user_long_message_displayed";
62     public static final int MESSAGE_PADDING = 10;
63 
64     @Retention(RetentionPolicy.SOURCE)
65     @IntDef({EXIT_DIALOG, INITIAL_DIALOG, GRANT_ADMIN_DIALOG,
66             EDIT_NAME_DIALOG, CREATE_USER_AND_CLOSE})
67     public @interface AddUserState {}
68 
69     private static final int EXIT_DIALOG = -1;
70     private static final int INITIAL_DIALOG = 0;
71     private static final int GRANT_ADMIN_DIALOG = 1;
72     private static final int EDIT_NAME_DIALOG = 2;
73     private static final int CREATE_USER_AND_CLOSE = 3;
74 
75     private @AddUserState int mCurrentState;
76 
77     private CustomDialogHelper mCustomDialogHelper;
78 
79     private EditUserPhotoController mEditUserPhotoController;
80     private Bitmap mSavedPhoto;
81     private String mSavedName;
82     private Drawable mSavedDrawable;
83     private String mUserName;
84     private Drawable mNewUserIcon;
85     private Boolean mIsAdmin;
86     private Dialog mUserCreationDialog;
87     private View mGrantAdminView;
88     private View mEditUserInfoView;
89     private EditText mUserNameView;
90     private Activity mActivity;
91     private ActivityStarter mActivityStarter;
92     private boolean mWaitingForActivityResult;
93     private NewUserData mSuccessCallback;
94     private Runnable mCancelCallback;
95 
96     private final String mFileAuthority;
97 
CreateUserDialogController(String fileAuthority)98     public CreateUserDialogController(String fileAuthority) {
99         mFileAuthority = fileAuthority;
100     }
101 
102     /**
103      * Resets saved values.
104      */
clear()105     public void clear() {
106         mUserCreationDialog = null;
107         mCustomDialogHelper = null;
108         mEditUserPhotoController = null;
109         mSavedPhoto = null;
110         mSavedName = null;
111         mSavedDrawable = null;
112         mIsAdmin = null;
113         mActivity = null;
114         mActivityStarter = null;
115         mGrantAdminView = null;
116         mEditUserInfoView = null;
117         mUserNameView = null;
118         mSuccessCallback = null;
119         mCancelCallback = null;
120         mCurrentState = INITIAL_DIALOG;
121     }
122 
123     /**
124      * Notifies that the containing activity or fragment was reinitialized.
125      */
onRestoreInstanceState(Bundle savedInstanceState)126     public void onRestoreInstanceState(Bundle savedInstanceState) {
127         String pendingPhoto = savedInstanceState.getString(KEY_SAVED_PHOTO);
128         if (pendingPhoto != null) {
129             ThreadUtils.postOnBackgroundThread(() -> {
130                 mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap(
131                         new File(pendingPhoto));
132             });
133         }
134         mCurrentState = savedInstanceState.getInt(KEY_CURRENT_STATE);
135         if (savedInstanceState.containsKey(KEY_IS_ADMIN)) {
136             mIsAdmin = savedInstanceState.getBoolean(KEY_IS_ADMIN);
137         }
138         mSavedName = savedInstanceState.getString(KEY_SAVED_NAME);
139         mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false);
140     }
141 
142     /**
143      * Notifies that the containing activity or fragment is saving its state for later use.
144      */
onSaveInstanceState(Bundle savedInstanceState)145     public void onSaveInstanceState(Bundle savedInstanceState) {
146         if (mUserCreationDialog != null && mEditUserPhotoController != null) {
147             // Bitmap cannot be stored into bundle because it may exceed parcel limit
148             // Store it in a temporary file instead
149             ThreadUtils.postOnBackgroundThread(() -> {
150                 File file = mEditUserPhotoController.saveNewUserPhotoBitmap();
151                 if (file != null) {
152                     savedInstanceState.putString(KEY_SAVED_PHOTO, file.getPath());
153                 }
154             });
155         }
156         if (mIsAdmin != null) {
157             savedInstanceState.putBoolean(KEY_IS_ADMIN, Boolean.TRUE.equals(mIsAdmin));
158         }
159         savedInstanceState.putString(KEY_SAVED_NAME, mUserNameView.getText().toString().trim());
160         savedInstanceState.putInt(KEY_CURRENT_STATE, mCurrentState);
161         savedInstanceState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult);
162     }
163 
164     /**
165      * Notifies that an activity has started.
166      */
startingActivityForResult()167     public void startingActivityForResult() {
168         mWaitingForActivityResult = true;
169     }
170 
171     /**
172      * Notifies that the result from activity has been received.
173      */
onActivityResult(int requestCode, int resultCode, Intent data)174     public void onActivityResult(int requestCode, int resultCode, Intent data) {
175         mWaitingForActivityResult = false;
176         if (mEditUserPhotoController != null) {
177             mEditUserPhotoController.onActivityResult(requestCode, resultCode, data);
178         }
179     }
180 
181     /**
182      * Creates an add user dialog with option to set the user's name and photo and choose their
183      * admin status.
184      */
createDialog(Activity activity, ActivityStarter activityStarter, boolean isMultipleAdminEnabled, NewUserData successCallback, Runnable cancelCallback)185     public Dialog createDialog(Activity activity,
186             ActivityStarter activityStarter, boolean isMultipleAdminEnabled,
187             NewUserData successCallback, Runnable cancelCallback) {
188         mActivity = activity;
189         mCustomDialogHelper = new CustomDialogHelper(activity);
190         mSuccessCallback = successCallback;
191         mCancelCallback = cancelCallback;
192         mActivityStarter = activityStarter;
193         addCustomViews(isMultipleAdminEnabled);
194         mUserCreationDialog = mCustomDialogHelper.getDialog();
195         updateLayout();
196         mUserCreationDialog.setOnDismissListener(view -> finish());
197         mCustomDialogHelper.setMessagePadding(MESSAGE_PADDING);
198         mUserCreationDialog.setCanceledOnTouchOutside(true);
199         return mUserCreationDialog;
200     }
201 
addCustomViews(boolean isMultipleAdminEnabled)202     private void addCustomViews(boolean isMultipleAdminEnabled) {
203         addGrantAdminView();
204         addUserInfoEditView();
205         mCustomDialogHelper.setPositiveButton(R.string.next, view -> {
206             mCurrentState++;
207             if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) {
208                 mCurrentState++;
209             }
210             updateLayout();
211         });
212         mCustomDialogHelper.setNegativeButton(R.string.back, view -> {
213             mCurrentState--;
214             if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) {
215                 mCurrentState--;
216             }
217             updateLayout();
218         });
219     }
220 
updateLayout()221     private void updateLayout() {
222         switch (mCurrentState) {
223             case INITIAL_DIALOG:
224                 mEditUserInfoView.setVisibility(View.GONE);
225                 mGrantAdminView.setVisibility(View.GONE);
226                 final SharedPreferences preferences = mActivity.getPreferences(
227                         Context.MODE_PRIVATE);
228                 final boolean longMessageDisplayed = preferences.getBoolean(
229                         KEY_ADD_USER_LONG_MESSAGE_DISPLAYED, false);
230                 final int messageResId = longMessageDisplayed
231                         ? R.string.user_add_user_message_short
232                         : R.string.user_add_user_message_long;
233                 if (!longMessageDisplayed) {
234                     preferences.edit().putBoolean(
235                             KEY_ADD_USER_LONG_MESSAGE_DISPLAYED,
236                             true).apply();
237                 }
238                 Drawable icon = mActivity.getDrawable(R.drawable.ic_person_add);
239                 mCustomDialogHelper.setVisibility(mCustomDialogHelper.ICON, true)
240                         .setVisibility(mCustomDialogHelper.MESSAGE, true)
241                         .setIcon(icon)
242                         .setButtonEnabled(true)
243                         .setTitle(R.string.user_add_user_title)
244                         .setMessage(messageResId)
245                         .setNegativeButtonText(R.string.cancel)
246                         .setPositiveButtonText(R.string.next);
247                 break;
248             case GRANT_ADMIN_DIALOG:
249                 mEditUserInfoView.setVisibility(View.GONE);
250                 mGrantAdminView.setVisibility(View.VISIBLE);
251                 mCustomDialogHelper
252                         .setVisibility(mCustomDialogHelper.ICON, true)
253                         .setVisibility(mCustomDialogHelper.MESSAGE, true)
254                         .setIcon(mActivity.getDrawable(R.drawable.ic_admin_panel_settings))
255                         .setTitle(R.string.user_grant_admin_title)
256                         .setMessage(R.string.user_grant_admin_message)
257                         .setNegativeButtonText(R.string.back)
258                         .setPositiveButtonText(R.string.next);
259                 if (mIsAdmin == null) {
260                     mCustomDialogHelper.setButtonEnabled(false);
261                 }
262                 break;
263             case EDIT_NAME_DIALOG:
264                 mCustomDialogHelper
265                         .setVisibility(mCustomDialogHelper.ICON, false)
266                         .setVisibility(mCustomDialogHelper.MESSAGE, false)
267                         .setTitle(R.string.user_info_settings_title)
268                         .setNegativeButtonText(R.string.back)
269                         .setPositiveButtonText(R.string.done);
270                 mEditUserInfoView.setVisibility(View.VISIBLE);
271                 mGrantAdminView.setVisibility(View.GONE);
272                 break;
273             case CREATE_USER_AND_CLOSE:
274                 mNewUserIcon = mEditUserPhotoController != null
275                         ? mEditUserPhotoController.getNewUserPhotoDrawable()
276                         : null;
277 
278                 String newName = mUserNameView.getText().toString().trim();
279                 String defaultName = mActivity.getString(R.string.user_new_user_name);
280                 mUserName = !newName.isEmpty() ? newName : defaultName;
281                 mCustomDialogHelper.getDialog().dismiss();
282                 break;
283             case EXIT_DIALOG:
284                 mCustomDialogHelper.getDialog().dismiss();
285                 break;
286             default:
287                 if (mCurrentState < EXIT_DIALOG) {
288                     mCurrentState = EXIT_DIALOG;
289                     updateLayout();
290                 } else {
291                     mCurrentState = CREATE_USER_AND_CLOSE;
292                     updateLayout();
293                 }
294                 break;
295         }
296     }
297 
getUserIcon(Drawable defaultUserIcon)298     private Drawable getUserIcon(Drawable defaultUserIcon) {
299         if (mSavedPhoto != null) {
300             mSavedDrawable = CircleFramedDrawable.getInstance(mActivity, mSavedPhoto);
301             return mSavedDrawable;
302         }
303         return defaultUserIcon;
304     }
305 
addUserInfoEditView()306     private void addUserInfoEditView() {
307         mEditUserInfoView = View.inflate(mActivity, R.layout.edit_user_info_dialog_content, null);
308         mCustomDialogHelper.addCustomView(mEditUserInfoView);
309         setUserName();
310         ImageView userPhotoView = mEditUserInfoView.findViewById(R.id.user_photo);
311 
312         // if oldUserIcon param is null then we use a default gray user icon
313         Drawable defaultUserIcon = UserIcons.getDefaultUserIcon(
314                 mActivity.getResources(), UserHandle.USER_NULL, false);
315         // in case a new photo was selected and the activity got recreated we have to load the image
316         Drawable userIcon = getUserIcon(defaultUserIcon);
317         userPhotoView.setImageDrawable(userIcon);
318 
319         if (isChangePhotoRestrictedByBase(mActivity)) {
320             // some users can't change their photos so we need to remove the suggestive icon
321             mEditUserInfoView.findViewById(R.id.add_a_photo_icon).setVisibility(View.GONE);
322         } else {
323             RestrictedLockUtils.EnforcedAdmin adminRestriction =
324                     getChangePhotoAdminRestriction(mActivity);
325             if (adminRestriction != null) {
326                 userPhotoView.setOnClickListener(view ->
327                         RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
328                                 mActivity, adminRestriction));
329             } else {
330                 mEditUserPhotoController = createEditUserPhotoController(userPhotoView);
331             }
332         }
333     }
334 
setUserName()335     private void setUserName() {
336         mUserNameView = mEditUserInfoView.findViewById(R.id.user_name);
337         if (mSavedName == null) {
338             mUserNameView.setText(R.string.user_new_user_name);
339         } else {
340             mUserNameView.setText(mSavedName);
341         }
342     }
343 
addGrantAdminView()344     private void addGrantAdminView() {
345         mGrantAdminView = View.inflate(mActivity, R.layout.grant_admin_dialog_content, null);
346         mCustomDialogHelper.addCustomView(mGrantAdminView);
347         RadioGroup radioGroup = mGrantAdminView.findViewById(R.id.choose_admin);
348         radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
349                     mCustomDialogHelper.setButtonEnabled(true);
350                     mIsAdmin = checkedId == R.id.grant_admin_yes;
351                 }
352         );
353         if (Boolean.TRUE.equals(mIsAdmin)) {
354             RadioButton button = radioGroup.findViewById(R.id.grant_admin_yes);
355             button.setChecked(true);
356         } else if (Boolean.FALSE.equals(mIsAdmin)) {
357             RadioButton button = radioGroup.findViewById(R.id.grant_admin_no);
358             button.setChecked(true);
359         }
360     }
361 
362     @VisibleForTesting
isChangePhotoRestrictedByBase(Context context)363     boolean isChangePhotoRestrictedByBase(Context context) {
364         return RestrictedLockUtilsInternal.hasBaseUserRestriction(
365                 context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId());
366     }
367 
368     @VisibleForTesting
getChangePhotoAdminRestriction(Context context)369     RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) {
370         return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
371                 context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId());
372     }
373 
374     @VisibleForTesting
createEditUserPhotoController(ImageView userPhotoView)375     EditUserPhotoController createEditUserPhotoController(ImageView userPhotoView) {
376         return new EditUserPhotoController(mActivity, mActivityStarter, userPhotoView,
377                 mSavedPhoto, mSavedDrawable, mFileAuthority);
378     }
379 
isActive()380     public boolean isActive() {
381         return mCustomDialogHelper != null && mCustomDialogHelper.getDialog() != null;
382     }
383 
384     /**
385      * Runs callback and clears saved values after dialog is dismissed.
386      */
finish()387     public void finish() {
388         if (mCurrentState == CREATE_USER_AND_CLOSE) {
389             if (mSuccessCallback != null) {
390                 mSuccessCallback.onSuccess(mUserName, mNewUserIcon, Boolean.TRUE.equals(mIsAdmin));
391             }
392         } else {
393             if (mCancelCallback != null) {
394                 mCancelCallback.run();
395             }
396         }
397         clear();
398     }
399 }
400