From 2ac45dd49e62242d5230b15152bd27d790da9041 Mon Sep 17 00:00:00 2001 From: Andras Kloczl Date: Mon, 10 Aug 2020 00:51:57 +0100 Subject: [PATCH] Add user name photo dialog to user creation in SysUI - Move user creation dialog related resources to SettingsLib - Change dialog showing logic in UserSettings because EditUserInfoController contracts have changed. - Show UserCreatingDialog when user is being created - Fix crash when phone is rotated during user creation Test: manual test Doc: http://shortn/_cJE9o6pBZR Screenrecord: http://shortn/_Jy5Q0lTAUL Bug: 147653252 Change-Id: I15e15ad88b768a5b679de32c5429d921d850a3cb --- res/layout/edit_user_info_dialog_content.xml | 49 -- res/layout/restricted_popup_menu_item.xml | 44 -- res/values/dimens.xml | 3 - res/values/strings.xml | 9 - .../users/EditUserInfoController.java | 238 --------- .../users/EditUserPhotoController.java | 465 ------------------ .../settings/users/PhotoCapabilityUtils.java | 56 --- .../android/settings/users/UserSettings.java | 149 +++--- .../users/EditUserInfoControllerTest.java | 282 ----------- 9 files changed, 79 insertions(+), 1216 deletions(-) delete mode 100644 res/layout/edit_user_info_dialog_content.xml delete mode 100644 res/layout/restricted_popup_menu_item.xml delete mode 100644 src/com/android/settings/users/EditUserInfoController.java delete mode 100644 src/com/android/settings/users/EditUserPhotoController.java delete mode 100644 src/com/android/settings/users/PhotoCapabilityUtils.java delete mode 100644 tests/robotests/src/com/android/settings/users/EditUserInfoControllerTest.java diff --git a/res/layout/edit_user_info_dialog_content.xml b/res/layout/edit_user_info_dialog_content.xml deleted file mode 100644 index 2bd464b4233..00000000000 --- a/res/layout/edit_user_info_dialog_content.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - diff --git a/res/layout/restricted_popup_menu_item.xml b/res/layout/restricted_popup_menu_item.xml deleted file mode 100644 index 636e3f91847..00000000000 --- a/res/layout/restricted_popup_menu_item.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 799aa226d0b..a561df2a86c 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -51,9 +51,6 @@ 16dp 3dp - - 300dip - 200dp 200dp diff --git a/res/values/strings.xml b/res/values/strings.xml index c3d525609b3..f6c69d3470b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6990,8 +6990,6 @@ Admin You (%s) - - Nickname You can add up to %1$d users @@ -7429,13 +7427,6 @@ Finish - - Take a photo - - Choose an image - - Select photo - diff --git a/src/com/android/settings/users/EditUserInfoController.java b/src/com/android/settings/users/EditUserInfoController.java deleted file mode 100644 index 6b5e670609b..00000000000 --- a/src/com/android/settings/users/EditUserInfoController.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.users; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.UserInfo; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.UserHandle; -import android.os.UserManager; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.WindowManager; -import android.widget.EditText; -import android.widget.ImageView; - -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.Fragment; - -import com.android.settings.R; -import com.android.settingslib.drawable.CircleFramedDrawable; - -import java.io.File; - -/** - * This class encapsulates a Dialog for editing the user nickname and photo. - */ -public class EditUserInfoController { - - private static final String KEY_AWAITING_RESULT = "awaiting_result"; - private static final String KEY_SAVED_PHOTO = "pending_photo"; - - private Dialog mEditUserInfoDialog; - private Bitmap mSavedPhoto; - private EditUserPhotoController mEditUserPhotoController; - private UserHandle mUser; - private UserManager mUserManager; - private boolean mWaitingForActivityResult = false; - - /** - * Callback made when either the username text or photo choice changes. - */ - public interface OnContentChangedCallback { - /** Photo updated. */ - void onPhotoChanged(UserHandle user, Drawable photo); - /** Username updated. */ - void onLabelChanged(UserHandle user, CharSequence label); - } - - /** - * Callback made when the dialog finishes. - */ - public interface OnDialogCompleteCallback { - /** Dialog closed with positive button. */ - void onPositive(); - /** Dialog closed with negative button or cancelled. */ - void onNegativeOrCancel(); - } - - public void clear() { - if (mEditUserPhotoController != null) { - mEditUserPhotoController.removeNewUserPhotoBitmapFile(); - } - mEditUserInfoDialog = null; - mSavedPhoto = null; - } - - public Dialog getDialog() { - return mEditUserInfoDialog; - } - - public void onRestoreInstanceState(Bundle icicle) { - String pendingPhoto = icicle.getString(KEY_SAVED_PHOTO); - if (pendingPhoto != null) { - mSavedPhoto = EditUserPhotoController.loadNewUserPhotoBitmap(new File(pendingPhoto)); - } - mWaitingForActivityResult = icicle.getBoolean(KEY_AWAITING_RESULT, false); - } - - public void onSaveInstanceState(Bundle outState) { - if (mEditUserInfoDialog != null && mEditUserPhotoController != null) { - // Bitmap cannot be stored into bundle because it may exceed parcel limit - // Store it in a temporary file instead - File file = mEditUserPhotoController.saveNewUserPhotoBitmap(); - if (file != null) { - outState.putString(KEY_SAVED_PHOTO, file.getPath()); - } - } - if (mWaitingForActivityResult) { - outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); - } - } - - public void startingActivityForResult() { - mWaitingForActivityResult = true; - } - - public void onActivityResult(int requestCode, int resultCode, Intent data) { - mWaitingForActivityResult = false; - - if (mEditUserPhotoController != null && mEditUserInfoDialog != null) { - mEditUserPhotoController.onActivityResult(requestCode, resultCode, data); - } - } - - public Dialog createDialog(final Fragment fragment, final Drawable currentUserIcon, - final CharSequence currentUserName, - String title, final OnContentChangedCallback callback, UserHandle user, - OnDialogCompleteCallback completeCallback) { - Activity activity = fragment.getActivity(); - mUser = user; - if (mUserManager == null) { - mUserManager = activity.getSystemService(UserManager.class); - } - LayoutInflater inflater = activity.getLayoutInflater(); - View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null); - - final EditText userNameView = (EditText) content.findViewById(R.id.user_name); - userNameView.setText(currentUserName); - - final ImageView userPhotoView = (ImageView) content.findViewById(R.id.user_photo); - - boolean canChangePhoto = mUserManager != null && - canChangePhoto(activity, mUserManager.getUserInfo(user.getIdentifier())); - if (!canChangePhoto) { - // some users can't change their photos so we need to remove suggestive - // background from the photoView - userPhotoView.setBackground(null); - } - Drawable drawable = null; - if (mSavedPhoto != null) { - drawable = CircleFramedDrawable.getInstance(activity, mSavedPhoto); - } else { - drawable = currentUserIcon; - } - userPhotoView.setImageDrawable(drawable); - if (canChangePhoto) { - mEditUserPhotoController = - createEditUserPhotoController(fragment, userPhotoView, drawable); - } - mEditUserInfoDialog = new AlertDialog.Builder(activity) - .setTitle(title) - .setView(content) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == DialogInterface.BUTTON_POSITIVE) { - // Update the name if changed. - CharSequence userName = userNameView.getText(); - if (!TextUtils.isEmpty(userName)) { - if (currentUserName == null - || !userName.toString().equals( - currentUserName.toString())) { - if (callback != null) { - callback.onLabelChanged(mUser, userName.toString()); - } - } - } - // Update the photo if changed. - if (mEditUserPhotoController != null) { - Drawable drawable = - mEditUserPhotoController.getNewUserPhotoDrawable(); - if (drawable != null && !drawable.equals(currentUserIcon)) { - if (callback != null) { - callback.onPhotoChanged(mUser, drawable); - } - } - } - } - clear(); - if (completeCallback != null) { - completeCallback.onPositive(); - } - } - }) - .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - clear(); - if (completeCallback != null) { - completeCallback.onNegativeOrCancel(); - } - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - clear(); - if (completeCallback != null) { - completeCallback.onNegativeOrCancel(); - } - } - }) - .create(); - - // Make sure the IME is up. - mEditUserInfoDialog.getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); - - return mEditUserInfoDialog; - } - - @VisibleForTesting - boolean canChangePhoto(Context context, UserInfo user) { - return PhotoCapabilityUtils.canCropPhoto(context) && - (PhotoCapabilityUtils.canChoosePhoto(context) - || PhotoCapabilityUtils.canTakePhoto(context)); - } - - @VisibleForTesting - EditUserPhotoController createEditUserPhotoController(Fragment fragment, - ImageView userPhotoView, Drawable drawable) { - return new EditUserPhotoController(fragment, userPhotoView, - mSavedPhoto, drawable, mWaitingForActivityResult); - } -} diff --git a/src/com/android/settings/users/EditUserPhotoController.java b/src/com/android/settings/users/EditUserPhotoController.java deleted file mode 100644 index a20513a3664..00000000000 --- a/src/com/android/settings/users/EditUserPhotoController.java +++ /dev/null @@ -1,465 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.users; - -import android.app.Activity; -import android.content.ClipData; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.StrictMode; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.ContactsContract.DisplayPhoto; -import android.provider.MediaStore; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.ListPopupWindow; -import android.widget.TextView; - -import androidx.core.content.FileProvider; -import androidx.fragment.app.Fragment; - -import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.RestrictedLockUtilsInternal; -import com.android.settingslib.drawable.CircleFramedDrawable; - -import libcore.io.Streams; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -public class EditUserPhotoController { - private static final String TAG = "EditUserPhotoController"; - - // It seems that this class generates custom request codes and they may - // collide with ours, these values are very unlikely to have a conflict. - private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001; - private static final int REQUEST_CODE_TAKE_PHOTO = 1002; - private static final int REQUEST_CODE_CROP_PHOTO = 1003; - - private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; - private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto2.jpg"; - private static final String NEW_USER_PHOTO_FILE_NAME = "NewUserPhoto.png"; - - private final int mPhotoSize; - - private final Context mContext; - private final Fragment mFragment; - private final ImageView mImageView; - - private final Uri mCropPictureUri; - private final Uri mTakePictureUri; - - private Bitmap mNewUserPhotoBitmap; - private Drawable mNewUserPhotoDrawable; - - public EditUserPhotoController(Fragment fragment, ImageView view, - Bitmap bitmap, Drawable drawable, boolean waiting) { - mContext = view.getContext(); - mFragment = fragment; - mImageView = view; - mCropPictureUri = createTempImageUri(mContext, CROP_PICTURE_FILE_NAME, !waiting); - mTakePictureUri = createTempImageUri(mContext, TAKE_PICTURE_FILE_NAME, !waiting); - mPhotoSize = getPhotoSize(mContext); - mImageView.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - showUpdatePhotoPopup(); - } - }); - mNewUserPhotoBitmap = bitmap; - mNewUserPhotoDrawable = drawable; - } - - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return false; - } - final Uri pictureUri = data != null && data.getData() != null - ? data.getData() : mTakePictureUri; - switch (requestCode) { - case REQUEST_CODE_CROP_PHOTO: - onPhotoCropped(pictureUri, true); - return true; - case REQUEST_CODE_TAKE_PHOTO: - case REQUEST_CODE_CHOOSE_PHOTO: - if (mTakePictureUri.equals(pictureUri)) { - cropPhoto(); - } else { - copyAndCropPhoto(pictureUri); - } - return true; - } - return false; - } - - public Bitmap getNewUserPhotoBitmap() { - return mNewUserPhotoBitmap; - } - - public Drawable getNewUserPhotoDrawable() { - return mNewUserPhotoDrawable; - } - - private void showUpdatePhotoPopup() { - final Context context = mImageView.getContext(); - final boolean canTakePhoto = PhotoCapabilityUtils.canTakePhoto(context); - final boolean canChoosePhoto = PhotoCapabilityUtils.canChoosePhoto(context); - - if (!canTakePhoto && !canChoosePhoto) { - return; - } - - final List items = new ArrayList<>(); - - if (canTakePhoto) { - final String title = context.getString(R.string.user_image_take_photo); - final Runnable action = new Runnable() { - @Override - public void run() { - takePhoto(); - } - }; - items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON, - action)); - } - - if (canChoosePhoto) { - final String title = context.getString(R.string.user_image_choose_photo); - final Runnable action = new Runnable() { - @Override - public void run() { - choosePhoto(); - } - }; - items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON, - action)); - } - - final ListPopupWindow listPopupWindow = new ListPopupWindow(context); - - listPopupWindow.setAnchorView(mImageView); - listPopupWindow.setModal(true); - listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); - listPopupWindow.setAdapter(new RestrictedPopupMenuAdapter(context, items)); - - final int width = Math.max(mImageView.getWidth(), context.getResources() - .getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width)); - listPopupWindow.setWidth(width); - listPopupWindow.setDropDownGravity(Gravity.START); - - listPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - listPopupWindow.dismiss(); - final RestrictedMenuItem item = - (RestrictedMenuItem) parent.getAdapter().getItem(position); - item.doAction(); - } - }); - - listPopupWindow.show(); - } - - private void takePhoto() { - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - appendOutputExtra(intent, mTakePictureUri); - mFragment.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); - } - - private void choosePhoto() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); - intent.setType("image/*"); - appendOutputExtra(intent, mTakePictureUri); - mFragment.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); - } - - private void copyAndCropPhoto(final Uri pictureUri) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - final ContentResolver cr = mContext.getContentResolver(); - try (InputStream in = cr.openInputStream(pictureUri); - OutputStream out = cr.openOutputStream(mTakePictureUri)) { - Streams.copy(in, out); - } catch (IOException e) { - Log.w(TAG, "Failed to copy photo", e); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - if (!mFragment.isAdded()) return; - cropPhoto(); - } - }.execute(); - } - - private void cropPhoto() { - // TODO: Use a public intent, when there is one. - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(mTakePictureUri, "image/*"); - appendOutputExtra(intent, mCropPictureUri); - appendCropExtras(intent); - if (intent.resolveActivity(mContext.getPackageManager()) != null) { - try { - StrictMode.disableDeathOnFileUriExposure(); - mFragment.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); - } finally { - StrictMode.enableDeathOnFileUriExposure(); - } - } else { - onPhotoCropped(mTakePictureUri, false); - } - } - - private void appendOutputExtra(Intent intent, Uri pictureUri) { - intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri)); - } - - private void appendCropExtras(Intent intent) { - intent.putExtra("crop", "true"); - intent.putExtra("scale", true); - intent.putExtra("scaleUpIfNeeded", true); - intent.putExtra("aspectX", 1); - intent.putExtra("aspectY", 1); - intent.putExtra("outputX", mPhotoSize); - intent.putExtra("outputY", mPhotoSize); - } - - private void onPhotoCropped(final Uri data, final boolean cropped) { - new AsyncTask() { - @Override - protected Bitmap doInBackground(Void... params) { - if (cropped) { - InputStream imageStream = null; - try { - imageStream = mContext.getContentResolver() - .openInputStream(data); - return BitmapFactory.decodeStream(imageStream); - } catch (FileNotFoundException fe) { - Log.w(TAG, "Cannot find image file", fe); - return null; - } finally { - if (imageStream != null) { - try { - imageStream.close(); - } catch (IOException ioe) { - Log.w(TAG, "Cannot close image stream", ioe); - } - } - } - } else { - // Scale and crop to a square aspect ratio - Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, - Config.ARGB_8888); - Canvas canvas = new Canvas(croppedImage); - Bitmap fullImage = null; - try { - InputStream imageStream = mContext.getContentResolver() - .openInputStream(data); - fullImage = BitmapFactory.decodeStream(imageStream); - } catch (FileNotFoundException fe) { - return null; - } - if (fullImage != null) { - final int squareSize = Math.min(fullImage.getWidth(), - fullImage.getHeight()); - final int left = (fullImage.getWidth() - squareSize) / 2; - final int top = (fullImage.getHeight() - squareSize) / 2; - Rect rectSource = new Rect(left, top, - left + squareSize, top + squareSize); - Rect rectDest = new Rect(0, 0, mPhotoSize, mPhotoSize); - Paint paint = new Paint(); - canvas.drawBitmap(fullImage, rectSource, rectDest, paint); - return croppedImage; - } else { - // Bah! Got nothin. - return null; - } - } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null) { - mNewUserPhotoBitmap = bitmap; - mNewUserPhotoDrawable = CircleFramedDrawable - .getInstance(mImageView.getContext(), mNewUserPhotoBitmap); - mImageView.setImageDrawable(mNewUserPhotoDrawable); - } - new File(mContext.getCacheDir(), TAKE_PICTURE_FILE_NAME).delete(); - new File(mContext.getCacheDir(), CROP_PICTURE_FILE_NAME).delete(); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); - } - - private static int getPhotoSize(Context context) { - Cursor cursor = context.getContentResolver().query( - DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, - new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); - try { - cursor.moveToFirst(); - return cursor.getInt(0); - } finally { - cursor.close(); - } - } - - private Uri createTempImageUri(Context context, String fileName, boolean purge) { - final File folder = context.getCacheDir(); - folder.mkdirs(); - final File fullPath = new File(folder, fileName); - if (purge) { - fullPath.delete(); - } - return FileProvider.getUriForFile(context, Utils.FILE_PROVIDER_AUTHORITY, fullPath); - } - - File saveNewUserPhotoBitmap() { - if (mNewUserPhotoBitmap == null) { - return null; - } - try { - File file = new File(mContext.getCacheDir(), NEW_USER_PHOTO_FILE_NAME); - OutputStream os = new FileOutputStream(file); - mNewUserPhotoBitmap.compress(Bitmap.CompressFormat.PNG, 100, os); - os.flush(); - os.close(); - return file; - } catch (IOException e) { - Log.e(TAG, "Cannot create temp file", e); - } - return null; - } - - static Bitmap loadNewUserPhotoBitmap(File file) { - return BitmapFactory.decodeFile(file.getAbsolutePath()); - } - - void removeNewUserPhotoBitmapFile() { - new File(mContext.getCacheDir(), NEW_USER_PHOTO_FILE_NAME).delete(); - } - - private static final class RestrictedMenuItem { - private final Context mContext; - private final String mTitle; - private final Runnable mAction; - private final RestrictedLockUtils.EnforcedAdmin mAdmin; - // Restriction may be set by system or something else via UserManager.setUserRestriction(). - private final boolean mIsRestrictedByBase; - - /** - * The menu item, used for popup menu. Any element of such a menu can be disabled by admin. - * @param context A context. - * @param title The title of the menu item. - * @param restriction The restriction, that if is set, blocks the menu item. - * @param action The action on menu item click. - */ - public RestrictedMenuItem(Context context, String title, String restriction, - Runnable action) { - mContext = context; - mTitle = title; - mAction = action; - - final int myUserId = UserHandle.myUserId(); - mAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, - restriction, myUserId); - mIsRestrictedByBase = RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, - restriction, myUserId); - } - - @Override - public String toString() { - return mTitle; - } - - final void doAction() { - if (isRestrictedByBase()) { - return; - } - - if (isRestrictedByAdmin()) { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mAdmin); - return; - } - - mAction.run(); - } - - final boolean isRestrictedByAdmin() { - return mAdmin != null; - } - - final boolean isRestrictedByBase() { - return mIsRestrictedByBase; - } - } - - /** - * Provide this adapter to ListPopupWindow.setAdapter() to have a popup window menu, where - * any element can be restricted by admin (profile owner or device owner). - */ - private static final class RestrictedPopupMenuAdapter extends ArrayAdapter { - public RestrictedPopupMenuAdapter(Context context, List items) { - super(context, R.layout.restricted_popup_menu_item, R.id.text, items); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final View view = super.getView(position, convertView, parent); - final RestrictedMenuItem item = getItem(position); - final TextView text = (TextView) view.findViewById(R.id.text); - final ImageView image = (ImageView) view.findViewById(R.id.restricted_icon); - - text.setEnabled(!item.isRestrictedByAdmin() && !item.isRestrictedByBase()); - image.setVisibility(item.isRestrictedByAdmin() && !item.isRestrictedByBase() ? - ImageView.VISIBLE : ImageView.GONE); - - return view; - } - } -} diff --git a/src/com/android/settings/users/PhotoCapabilityUtils.java b/src/com/android/settings/users/PhotoCapabilityUtils.java deleted file mode 100644 index 1e0985737b0..00000000000 --- a/src/com/android/settings/users/PhotoCapabilityUtils.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.users; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.provider.MediaStore; - -class PhotoCapabilityUtils { - - /** - * Check if the current user can perform any activity for - * android.media.action.IMAGE_CAPTURE action. - */ - static boolean canTakePhoto(Context context) { - return context.getPackageManager().queryIntentActivities( - new Intent(MediaStore.ACTION_IMAGE_CAPTURE), - PackageManager.MATCH_DEFAULT_ONLY).size() > 0; - } - - /** - * Check if the current user can perform any activity for - * android.intent.action.GET_CONTENT action for images. - */ - static boolean canChoosePhoto(Context context) { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("image/*"); - return context.getPackageManager().queryIntentActivities(intent, 0).size() > 0; - } - - /** - * Check if the current user can perform any activity for - * com.android.camera.action.CROP action for images. - */ - static boolean canCropPhoto(Context context) { - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setType("image/*"); - return context.getPackageManager().queryIntentActivities(intent, 0).size() > 0; - } - -} diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 8bfac9108da..50cb5de508b 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -16,8 +16,6 @@ package com.android.settings.users; -import static android.os.Process.myUserHandle; - import android.app.Activity; import android.app.ActivityManager; import android.app.Dialog; @@ -39,10 +37,12 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.ContactsContract; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.view.Menu; @@ -73,6 +73,8 @@ import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.drawable.CircleFramedDrawable; import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.users.EditUserInfoController; +import com.android.settingslib.users.UserCreatingDialog; import com.android.settingslib.utils.ThreadUtils; import com.google.android.setupcompat.util.WizardManagerHelper; @@ -83,7 +85,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Random; /** * Screen that manages the list of users on the device. @@ -165,9 +166,11 @@ public class UserSettings extends SettingsPreferenceFragment private static SparseArray sDarkDefaultUserBitmapCache = new SparseArray<>(); private MultiUserSwitchBarController mSwitchBarController; - private EditUserInfoController mEditUserInfoController = new EditUserInfoController(); + private EditUserInfoController mEditUserInfoController = + new EditUserInfoController(Utils.FILE_PROVIDER_AUTHORITY); private AddUserWhenLockedPreferenceController mAddUserWhenLockedPreferenceController; private MultiUserFooterPreferenceController mMultiUserFooterPreferenceController; + private UserCreatingDialog mUserCreatingDialog; private CharSequence mPendingUserName; private Drawable mPendingUserIcon; @@ -175,6 +178,8 @@ public class UserSettings extends SettingsPreferenceFragment // A place to cache the generated default avatar private Drawable mDefaultIconDrawable; + // TODO: Replace current Handler solution to something that doesn't leak memory and works + // TODO: during a configuration change private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { @@ -321,9 +326,9 @@ public class UserSettings extends SettingsPreferenceFragment @Override public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); mEditUserInfoController.onSaveInstanceState(outState); outState.putInt(SAVE_REMOVING_USER, mRemovingUserId); + super.onSaveInstanceState(outState); } @Override @@ -471,11 +476,22 @@ public class UserSettings extends SettingsPreferenceFragment } private void onUserCreated(int userId) { + hideUserCreatingDialog(); + // prevent crash when config changes during user creation + if (getContext() == null) { + return; + } mAddingUser = false; UserInfo userInfo = mUserManager.getUserInfo(userId); openUserDetails(userInfo, true); } + private void hideUserCreatingDialog() { + if (mUserCreatingDialog != null && mUserCreatingDialog.isShowing()) { + mUserCreatingDialog.dismiss(); + } + } + private void openUserDetails(UserInfo userInfo, boolean newUser) { Bundle extras = new Bundle(); extras.putInt(UserDetailsSettings.EXTRA_USER_ID, userInfo.id); @@ -605,94 +621,82 @@ public class UserSettings extends SettingsPreferenceFragment return dlg; } case DIALOG_USER_PROFILE_EDITOR: { - UserHandle user = myUserHandle(); - UserInfo info = mUserManager.getUserInfo(user.getIdentifier()); - return mEditUserInfoController.createDialog( - this, - Utils.getUserIcon(getPrefContext(), mUserManager, info), - info.name, - getString(com.android.settingslib.R.string.profile_info_settings_title), - new EditUserInfoController.OnContentChangedCallback() { - @Override - public void onPhotoChanged(UserHandle user, Drawable photo) { - ThreadUtils.postOnBackgroundThread(new Runnable() { - @Override - public void run() { - mUserManager.setUserIcon(user.getIdentifier(), - UserIcons.convertToBitmap(photo)); - } - }); - mMePreference.setIcon(photo); - } - - @Override - public void onLabelChanged(UserHandle user, CharSequence label) { - mMePreference.setTitle(label.toString()); - mUserManager.setUserName(user.getIdentifier(), label.toString()); - } - }, - user, - null); + return buildEditCurrentUserDialog(); } case DIALOG_USER_PROFILE_EDITOR_ADD_USER: { synchronized (mUserLock) { - mPendingUserIcon = UserIcons.getDefaultUserIcon(getPrefContext().getResources(), - new Random(System.currentTimeMillis()).nextInt(8), false); mPendingUserName = getString( com.android.settingslib.R.string.user_new_user_name); + mPendingUserIcon = null; } - return buildAddUserProfileEditorDialog(USER_TYPE_USER); + return buildAddUserDialog(USER_TYPE_USER); } case DIALOG_USER_PROFILE_EDITOR_ADD_RESTRICTED_PROFILE: { synchronized (mUserLock) { - mPendingUserIcon = UserIcons.getDefaultUserIcon(getPrefContext().getResources(), - new Random(System.currentTimeMillis()).nextInt(8), false); mPendingUserName = getString( com.android.settingslib.R.string.user_new_profile_name); + mPendingUserIcon = null; } - return buildAddUserProfileEditorDialog(USER_TYPE_RESTRICTED_PROFILE); + return buildAddUserDialog(USER_TYPE_RESTRICTED_PROFILE); } default: return null; } } - private Dialog buildAddUserProfileEditorDialog(int userType) { + private Dialog buildEditCurrentUserDialog() { + final Activity activity = getActivity(); + if (activity == null) { + return null; + } + + UserInfo user = mUserManager.getUserInfo(Process.myUserHandle().getIdentifier()); + Drawable userIcon = Utils.getUserIcon(activity, mUserManager, user); + + return mEditUserInfoController.createDialog( + activity, + this::startActivityForResult, + userIcon, + user.name, + getString(com.android.settingslib.R.string.profile_info_settings_title), + (newUserName, newUserIcon) -> { + if (newUserIcon != userIcon) { + ThreadUtils.postOnBackgroundThread(() -> + mUserManager.setUserIcon(user.id, + UserIcons.convertToBitmap(newUserIcon))); + mMePreference.setIcon(newUserIcon); + } + + if (!TextUtils.isEmpty(newUserName) && !newUserName.equals(user.name)) { + mMePreference.setTitle(newUserName); + mUserManager.setUserName(user.id, newUserName); + } + }, null); + } + + private Dialog buildAddUserDialog(int userType) { Dialog d; synchronized (mUserLock) { d = mEditUserInfoController.createDialog( - this, - mPendingUserIcon, - mPendingUserName, + getActivity(), + this::startActivityForResult, + null, + mPendingUserName.toString(), getString(userType == USER_TYPE_USER ? com.android.settingslib.R.string.user_info_settings_title : com.android.settingslib.R.string.profile_info_settings_title), - new EditUserInfoController.OnContentChangedCallback() { - @Override - public void onPhotoChanged(UserHandle user, Drawable photo) { - mPendingUserIcon = photo; - } - - @Override - public void onLabelChanged(UserHandle user, CharSequence label) { - mPendingUserName = label; - } + (userName, userIcon) -> { + mPendingUserIcon = userIcon; + mPendingUserName = userName; + addUserNow(userType); }, - myUserHandle(), - new EditUserInfoController.OnDialogCompleteCallback() { - @Override - public void onPositive() { - addUserNow(userType); + () -> { + synchronized (mUserLock) { + mPendingUserIcon = null; + mPendingUserName = null; } - - @Override - public void onNegativeOrCancel() { - synchronized (mUserLock) { - mPendingUserIcon = null; - mPendingUserName = null; - } - } - }); + } + ); } return d; } @@ -759,6 +763,9 @@ public class UserSettings extends SettingsPreferenceFragment : (mPendingUserName != null ? mPendingUserName.toString() : getString(R.string.user_new_profile_name)); } + + mUserCreatingDialog = new UserCreatingDialog(getActivity()); + mUserCreatingDialog.show(); ThreadUtils.postOnBackgroundThread(new Runnable() { @Override public void run() { @@ -781,13 +788,15 @@ public class UserSettings extends SettingsPreferenceFragment mAddingUser = false; mPendingUserIcon = null; mPendingUserName = null; + ThreadUtils.postOnMainThread(() -> hideUserCreatingDialog()); return; } - if (mPendingUserIcon != null) { - mUserManager.setUserIcon(user.id, - UserIcons.convertToBitmap(mPendingUserIcon)); + Drawable newUserIcon = mPendingUserIcon; + if (newUserIcon == null) { + newUserIcon = UserIcons.getDefaultUserIcon(getResources(), user.id, false); } + mUserManager.setUserIcon(user.id, UserIcons.convertToBitmap(newUserIcon)); if (userType == USER_TYPE_USER) { mHandler.sendEmptyMessage(MESSAGE_UPDATE_LIST); diff --git a/tests/robotests/src/com/android/settings/users/EditUserInfoControllerTest.java b/tests/robotests/src/com/android/settings/users/EditUserInfoControllerTest.java deleted file mode 100644 index db9872fc49f..00000000000 --- a/tests/robotests/src/com/android/settings/users/EditUserInfoControllerTest.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.users; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.Dialog; -import android.content.Context; -import android.content.Intent; -import android.content.pm.UserInfo; -import android.graphics.drawable.Drawable; -import android.widget.EditText; -import android.widget.ImageView; - -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; - -import com.android.settings.R; -import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.controller.ActivityController; -import org.robolectric.annotation.Config; - -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@RunWith(RobolectricTestRunner.class) -public class EditUserInfoControllerTest { - private static final int MAX_USER_NAME_LENGTH = 100; - - @Mock - private Fragment mFragment; - @Mock - private Drawable mCurrentIcon; - - private boolean mCanChangePhoto; - - private FragmentActivity mActivity; - private TestEditUserInfoController mController; - - public class TestEditUserInfoController extends EditUserInfoController { - private EditUserPhotoController mPhotoController; - - private EditUserPhotoController getPhotoController() { - return mPhotoController; - } - - @Override - protected EditUserPhotoController createEditUserPhotoController(Fragment fragment, - ImageView userPhotoView, Drawable drawable) { - mPhotoController = mock(EditUserPhotoController.class, Answers.RETURNS_DEEP_STUBS); - return mPhotoController; - } - - @Override - boolean canChangePhoto(Context context, UserInfo user) { - return mCanChangePhoto; - } - } - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mActivity = spy(ActivityController.of(new FragmentActivity()).get()); - when(mFragment.getActivity()).thenReturn(mActivity); - mController = new TestEditUserInfoController(); - mCanChangePhoto = true; - } - - @Test - public void photoControllerOnActivityResult_whenWaiting_isCalled() { - mController.createDialog(mFragment, mCurrentIcon, "test user", - "title", null, - android.os.Process.myUserHandle(), null); - mController.startingActivityForResult(); - Intent resultData = new Intent(); - mController.onActivityResult(0, 0, resultData); - EditUserPhotoController photoController = mController.getPhotoController(); - assertThat(photoController).isNotNull(); - verify(photoController).onActivityResult(eq(0), eq(0), same(resultData)); - } - - @Test - @Config(shadows = ShadowAlertDialogCompat.class) - public void userNameView_inputLongName_shouldBeConstrained() { - // generate a string of 200 'A's - final String longName = Stream.generate( - () -> String.valueOf('A')).limit(200).collect(Collectors.joining()); - final AlertDialog dialog = (AlertDialog) mController.createDialog(mFragment, mCurrentIcon, - "test user", "title", null, - android.os.Process.myUserHandle(), null); - final EditText userName = ShadowAlertDialogCompat.shadowOf(dialog).getView() - .findViewById(R.id.user_name); - - userName.setText(longName); - - assertThat(userName.getText().length()).isEqualTo(MAX_USER_NAME_LENGTH); - } - - @Test - public void onDialogCompleteCallback_isCalled_whenCancelled() { - EditUserInfoController.OnContentChangedCallback contentChangeCallback = mock( - EditUserInfoController.OnContentChangedCallback.class); - - EditUserInfoController.OnDialogCompleteCallback dialogCompleteCallback = mock( - EditUserInfoController.OnDialogCompleteCallback.class); - - AlertDialog dialog = (AlertDialog) mController.createDialog( - mFragment, mCurrentIcon, "test", - "title", contentChangeCallback, - android.os.Process.myUserHandle(), - dialogCompleteCallback); - - dialog.show(); - dialog.cancel(); - - verify(contentChangeCallback, times(0)) - .onLabelChanged(any(), any()); - verify(contentChangeCallback, times(0)) - .onPhotoChanged(any(), any()); - verify(dialogCompleteCallback, times(0)).onPositive(); - verify(dialogCompleteCallback, times(1)).onNegativeOrCancel(); - } - - @Test - public void onDialogCompleteCallback_isCalled_whenPositiveClicked() { - EditUserInfoController.OnContentChangedCallback contentChangeCallback = mock( - EditUserInfoController.OnContentChangedCallback.class); - - EditUserInfoController.OnDialogCompleteCallback dialogCompleteCallback = mock( - EditUserInfoController.OnDialogCompleteCallback.class); - - AlertDialog dialog = (AlertDialog) mController.createDialog( - mFragment, mCurrentIcon, "test", - "title", contentChangeCallback, - android.os.Process.myUserHandle(), - dialogCompleteCallback); - - // No change to the photo. - when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(mCurrentIcon); - - dialog.show(); - dialog.getButton(Dialog.BUTTON_POSITIVE).performClick(); - - verify(contentChangeCallback, times(0)) - .onLabelChanged(any(), any()); - verify(contentChangeCallback, times(0)) - .onPhotoChanged(any(), any()); - verify(dialogCompleteCallback, times(1)).onPositive(); - verify(dialogCompleteCallback, times(0)).onNegativeOrCancel(); - } - - @Test - public void onDialogCompleteCallback_isCalled_whenNegativeClicked() { - EditUserInfoController.OnContentChangedCallback contentChangeCallback = mock( - EditUserInfoController.OnContentChangedCallback.class); - - EditUserInfoController.OnDialogCompleteCallback dialogCompleteCallback = mock( - EditUserInfoController.OnDialogCompleteCallback.class); - - AlertDialog dialog = (AlertDialog) mController.createDialog( - mFragment, mCurrentIcon, "test", - "title", contentChangeCallback, - android.os.Process.myUserHandle(), - dialogCompleteCallback); - - dialog.show(); - dialog.getButton(Dialog.BUTTON_NEGATIVE).performClick(); - - verify(contentChangeCallback, times(0)) - .onLabelChanged(any(), any()); - verify(contentChangeCallback, times(0)) - .onPhotoChanged(any(), any()); - verify(dialogCompleteCallback, times(0)).onPositive(); - verify(dialogCompleteCallback, times(1)).onNegativeOrCancel(); - } - - @Test - public void onContentChangedCallback_isCalled_whenLabelChanges() { - EditUserInfoController.OnContentChangedCallback contentChangeCallback = mock( - EditUserInfoController.OnContentChangedCallback.class); - - EditUserInfoController.OnDialogCompleteCallback dialogCompleteCallback = mock( - EditUserInfoController.OnDialogCompleteCallback.class); - - AlertDialog dialog = (AlertDialog) mController.createDialog( - mFragment, mCurrentIcon, "test", - "title", contentChangeCallback, - android.os.Process.myUserHandle(), - dialogCompleteCallback); - - // No change to the photo. - when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(mCurrentIcon); - - dialog.show(); - String expectedNewName = "new test user"; - EditText editText = (EditText) dialog.findViewById(R.id.user_name); - editText.setText(expectedNewName); - - dialog.getButton(Dialog.BUTTON_POSITIVE).performClick(); - - verify(contentChangeCallback, times(1)) - .onLabelChanged(any(), eq(expectedNewName)); - verify(contentChangeCallback, times(0)) - .onPhotoChanged(any(), any()); - verify(dialogCompleteCallback, times(1)).onPositive(); - verify(dialogCompleteCallback, times(0)).onNegativeOrCancel(); - } - - @Test - public void onContentChangedCallback_isCalled_whenPhotoChanges() { - EditUserInfoController.OnContentChangedCallback contentChangeCallback = mock( - EditUserInfoController.OnContentChangedCallback.class); - - EditUserInfoController.OnDialogCompleteCallback dialogCompleteCallback = mock( - EditUserInfoController.OnDialogCompleteCallback.class); - - AlertDialog dialog = (AlertDialog) mController.createDialog( - mFragment, mCurrentIcon, "test", - "title", contentChangeCallback, - android.os.Process.myUserHandle(), - dialogCompleteCallback); - - // A different drawable. - Drawable newPhoto = mock(Drawable.class); - when(mController.getPhotoController().getNewUserPhotoDrawable()).thenReturn(newPhoto); - - dialog.show(); - dialog.getButton(Dialog.BUTTON_POSITIVE).performClick(); - - verify(contentChangeCallback, times(0)) - .onLabelChanged(any(), any()); - verify(contentChangeCallback, times(1)) - .onPhotoChanged(any(), eq(newPhoto)); - verify(dialogCompleteCallback, times(1)).onPositive(); - verify(dialogCompleteCallback, times(0)).onNegativeOrCancel(); - } - - @Test - public void createDialog_canNotChangePhoto_nullPhotoController() { - mCanChangePhoto = false; - - mController.createDialog( - mFragment, mCurrentIcon, "test", - "title", null, - android.os.Process.myUserHandle(), - null); - - assertThat(mController.mPhotoController).isNull(); - } -}