1 /* 2 * Copyright (C) 2013 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.app.Activity; 20 import android.app.Dialog; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.graphics.Bitmap; 24 import android.graphics.drawable.Drawable; 25 import android.os.Bundle; 26 import android.os.UserHandle; 27 import android.os.UserManager; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.WindowManager; 31 import android.widget.EditText; 32 import android.widget.ImageView; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 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 45 import java.io.File; 46 import java.util.function.BiConsumer; 47 48 /** 49 * This class encapsulates a Dialog for editing the user nickname and photo. 50 */ 51 public class EditUserInfoController { 52 53 private static final String KEY_AWAITING_RESULT = "awaiting_result"; 54 private static final String KEY_SAVED_PHOTO = "pending_photo"; 55 56 private Dialog mEditUserInfoDialog; 57 private Bitmap mSavedPhoto; 58 private Drawable mSavedDrawable; 59 private EditUserPhotoController mEditUserPhotoController; 60 private boolean mWaitingForActivityResult = false; 61 private final String mFileAuthority; 62 EditUserInfoController(String fileAuthority)63 public EditUserInfoController(String fileAuthority) { 64 mFileAuthority = fileAuthority; 65 } 66 clear()67 private void clear() { 68 if (mEditUserPhotoController != null) { 69 mEditUserPhotoController.removeNewUserPhotoBitmapFile(); 70 } 71 mEditUserInfoDialog = null; 72 mSavedPhoto = null; 73 mSavedDrawable = null; 74 } 75 76 /** 77 * This should be called when the container activity/fragment got re-initialized from a 78 * previously saved state. 79 */ onRestoreInstanceState(Bundle icicle)80 public void onRestoreInstanceState(Bundle icicle) { 81 String pendingPhoto = icicle.getString(KEY_SAVED_PHOTO); 82 if (pendingPhoto != null) { 83 mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap(new File(pendingPhoto)); 84 } 85 mWaitingForActivityResult = icicle.getBoolean(KEY_AWAITING_RESULT, false); 86 } 87 88 /** 89 * Should be called from the container activity/fragment when it's onSaveInstanceState is 90 * called. 91 */ onSaveInstanceState(Bundle outState)92 public void onSaveInstanceState(Bundle outState) { 93 if (mEditUserInfoDialog != null && mEditUserPhotoController != null) { 94 // Bitmap cannot be stored into bundle because it may exceed parcel limit 95 // Store it in a temporary file instead 96 File file = mEditUserPhotoController.saveNewUserPhotoBitmap(); 97 if (file != null) { 98 outState.putString(KEY_SAVED_PHOTO, file.getPath()); 99 } 100 } 101 outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); 102 } 103 104 /** 105 * Should be called from the container activity/fragment when an activity has started for 106 * take/choose/crop photo actions. 107 */ startingActivityForResult()108 public void startingActivityForResult() { 109 mWaitingForActivityResult = true; 110 } 111 112 /** 113 * Should be called from the container activity/fragment after it receives a result from 114 * take/choose/crop photo activity. 115 */ onActivityResult(int requestCode, int resultCode, Intent data)116 public void onActivityResult(int requestCode, int resultCode, Intent data) { 117 mWaitingForActivityResult = false; 118 119 if (mEditUserPhotoController != null && mEditUserInfoDialog != null) { 120 mEditUserPhotoController.onActivityResult(requestCode, resultCode, data); 121 } 122 } 123 124 /** 125 * Creates a user edit dialog with option to change the user's name and photo. 126 * 127 * @param activityStarter - ActivityStarter is called with appropriate intents and request 128 * codes to take photo/choose photo/crop photo. 129 */ createDialog(@onNull Activity activity, @NonNull ActivityStarter activityStarter, @Nullable Drawable oldUserIcon, @Nullable String defaultUserName, @Nullable BiConsumer<String, Drawable> successCallback, @Nullable Runnable cancelCallback)130 public @NonNull Dialog createDialog(@NonNull Activity activity, 131 @NonNull ActivityStarter activityStarter, @Nullable Drawable oldUserIcon, 132 @Nullable String defaultUserName, 133 @Nullable BiConsumer<String, Drawable> successCallback, 134 @Nullable Runnable cancelCallback) { 135 LayoutInflater inflater = LayoutInflater.from(activity); 136 View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null); 137 138 EditText userNameView = content.findViewById(R.id.user_name); 139 userNameView.setText(defaultUserName); 140 141 ImageView userPhotoView = content.findViewById(R.id.user_photo); 142 143 // if oldUserIcon param is null then we use a default gray user icon 144 Drawable defaultUserIcon = oldUserIcon != null ? oldUserIcon : UserIcons.getDefaultUserIcon( 145 activity.getResources(), UserHandle.USER_NULL, false); 146 // in case a new photo was selected and the activity got recreated we have to load the image 147 Drawable userIcon = getUserIcon(activity, defaultUserIcon); 148 userPhotoView.setImageDrawable(userIcon); 149 150 if (isChangePhotoRestrictedByBase(activity)) { 151 // some users can't change their photos so we need to remove the suggestive icon 152 content.findViewById(R.id.add_a_photo_icon).setVisibility(View.GONE); 153 } else { 154 RestrictedLockUtils.EnforcedAdmin adminRestriction = 155 getChangePhotoAdminRestriction(activity); 156 if (adminRestriction != null) { 157 userPhotoView.setOnClickListener(view -> 158 RestrictedLockUtils.sendShowAdminSupportDetailsIntent( 159 activity, adminRestriction)); 160 } else { 161 mEditUserPhotoController = createEditUserPhotoController(activity, activityStarter, 162 userPhotoView); 163 } 164 } 165 mEditUserInfoDialog = buildDialog(activity, content, userNameView, oldUserIcon, 166 defaultUserName, successCallback, cancelCallback); 167 168 // Make sure the IME is up. 169 mEditUserInfoDialog.getWindow() 170 .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 171 172 return mEditUserInfoDialog; 173 } 174 getUserIcon(Activity activity, Drawable defaultUserIcon)175 private Drawable getUserIcon(Activity activity, Drawable defaultUserIcon) { 176 if (mSavedPhoto != null) { 177 mSavedDrawable = CircleFramedDrawable.getInstance(activity, mSavedPhoto); 178 return mSavedDrawable; 179 } 180 return defaultUserIcon; 181 } 182 buildDialog(Activity activity, View content, EditText userNameView, @Nullable Drawable oldUserIcon, String defaultUserName, BiConsumer<String, Drawable> successCallback, Runnable cancelCallback)183 private Dialog buildDialog(Activity activity, View content, EditText userNameView, 184 @Nullable Drawable oldUserIcon, String defaultUserName, 185 BiConsumer<String, Drawable> successCallback, Runnable cancelCallback) { 186 CustomDialogHelper dialogHelper = new CustomDialogHelper(activity); 187 dialogHelper 188 .setTitle(R.string.user_info_settings_title) 189 .addCustomView(content) 190 .setPositiveButton(R.string.okay, view -> { 191 Drawable newUserIcon = mEditUserPhotoController != null 192 ? mEditUserPhotoController.getNewUserPhotoDrawable() 193 : null; 194 Drawable userIcon = newUserIcon != null 195 ? newUserIcon 196 : oldUserIcon; 197 198 String newName = userNameView.getText().toString().trim(); 199 String userName = !newName.isEmpty() ? newName : defaultUserName; 200 201 clear(); 202 if (successCallback != null) { 203 successCallback.accept(userName, userIcon); 204 } 205 dialogHelper.getDialog().dismiss(); 206 }) 207 .setBackButton(R.string.cancel, view -> { 208 clear(); 209 if (cancelCallback != null) { 210 cancelCallback.run(); 211 } 212 dialogHelper.getDialog().dismiss(); 213 }); 214 dialogHelper.getDialog().setOnCancelListener(dialog -> { 215 clear(); 216 if (cancelCallback != null) { 217 cancelCallback.run(); 218 } 219 dialogHelper.getDialog().dismiss(); 220 }); 221 return dialogHelper.getDialog(); 222 } 223 224 @VisibleForTesting isChangePhotoRestrictedByBase(Context context)225 boolean isChangePhotoRestrictedByBase(Context context) { 226 return RestrictedLockUtilsInternal.hasBaseUserRestriction( 227 context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId()); 228 } 229 230 @VisibleForTesting getChangePhotoAdminRestriction(Context context)231 RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) { 232 return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( 233 context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId()); 234 } 235 236 @VisibleForTesting createEditUserPhotoController(Activity activity, ActivityStarter activityStarter, ImageView userPhotoView)237 EditUserPhotoController createEditUserPhotoController(Activity activity, 238 ActivityStarter activityStarter, ImageView userPhotoView) { 239 return new EditUserPhotoController(activity, activityStarter, userPhotoView, 240 mSavedPhoto, mSavedDrawable, mFileAuthority, false); 241 } 242 } 243