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