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