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