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