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
This commit is contained in:
Andras Kloczl
2020-08-10 00:51:57 +01:00
parent 0488650aa0
commit 2ac45dd49e
9 changed files with 79 additions and 1216 deletions

View File

@@ -1,49 +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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:padding="16dip">
<ImageView
android:id="@+id/user_photo"
android:layout_width="56dip"
android:layout_height="56dip"
android:layout_gravity="bottom"
android:contentDescription="@string/user_image_photo_selector"
android:background="@*android:drawable/spinner_background_holo_dark"
android:scaleType="fitCenter"/>
<EditText
android:id="@+id/user_name"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_weight="1"
android:layout_marginStart="6dp"
android:minHeight="@dimen/min_tap_target_size"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textAlignment="viewStart"
android:inputType="text|textCapWords"
android:selectAllOnFocus="true"
android:hint="@string/user_nickname"
android:maxLength="100"/>
</LinearLayout>

View File

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2016, 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.
*/
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="16dip"
android:paddingEnd="16dip">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorAlertDialogListItem"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:ellipsize="marquee"
android:layout_alignParentLeft="true" />
<ImageView
android:id="@+id/restricted_icon"
android:layout_width="@*android:dimen/config_restrictedIconSize"
android:layout_height="@*android:dimen/config_restrictedIconSize"
android:scaleType="centerInside"
android:tint="?android:attr/colorAccent"
android:src="@*android:drawable/ic_info"
android:layout_alignParentRight="true"
android:visibility="gone" />
</RelativeLayout>

View File

@@ -51,9 +51,6 @@
<dimen name="pager_tabs_title_padding">16dp</dimen> <dimen name="pager_tabs_title_padding">16dp</dimen>
<dimen name="pager_tabs_selected_indicator_height">3dp</dimen> <dimen name="pager_tabs_selected_indicator_height">3dp</dimen>
<!-- Minimum width for the popup for updating a user's photo. -->
<dimen name="update_user_photo_popup_min_width">300dip</dimen>
<dimen name="captioning_preview_height">200dp</dimen> <dimen name="captioning_preview_height">200dp</dimen>
<dimen name="autoclick_preview_height">200dp</dimen> <dimen name="autoclick_preview_height">200dp</dimen>

View File

@@ -6990,8 +6990,6 @@
<string name="user_admin">Admin</string> <string name="user_admin">Admin</string>
<!-- User settings title for current user entry "You" user. [CHAR LIMIT=30] --> <!-- User settings title for current user entry "You" user. [CHAR LIMIT=30] -->
<string name="user_you">You (<xliff:g id="name" example="Name">%s</xliff:g>)</string> <string name="user_you">You (<xliff:g id="name" example="Name">%s</xliff:g>)</string>
<!-- Title for the preference to enter the nickname of the userto display in the user switcher [CHAR LIMIT=25]-->
<string name="user_nickname">Nickname</string>
<!-- Summary for add user action, when it's disabled [CHAR LIMIT=100] --> <!-- Summary for add user action, when it's disabled [CHAR LIMIT=100] -->
<string name="user_add_max_count">You can add up to <xliff:g id="user_count">%1$d</xliff:g> users</string> <string name="user_add_max_count">You can add up to <xliff:g id="user_count">%1$d</xliff:g> users</string>
@@ -7429,13 +7427,6 @@
<!-- Wizard finish button label [CHAR LIMIT=25] --> <!-- Wizard finish button label [CHAR LIMIT=25] -->
<string name="wizard_finish">Finish</string> <string name="wizard_finish">Finish</string>
<!-- An option in a photo selection dialog to take a new photo [CHAR LIMIT=50] -->
<string name="user_image_take_photo" msgid="7496128293167402354">Take a photo</string>
<!-- An option in a photo selection dialog to choose a pre-existing image [CHAR LIMIT=50] -->
<string name="user_image_choose_photo" msgid="3746334626214970837">Choose an image</string>
<!-- Accessibility message for the photo selector which is a button/popup with the current photo [CHAR LIMIT=50] -->
<string name="user_image_photo_selector">Select photo</string>
<!-- Text to display in regulatory info screen (from device overlay). --> <!-- Text to display in regulatory info screen (from device overlay). -->
<string name="regulatory_info_text"></string> <string name="regulatory_info_text"></string>

View File

@@ -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);
}
}

View File

@@ -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<EditUserPhotoController.RestrictedMenuItem> 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<Void, Void, Void>() {
@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<Void, Void, Bitmap>() {
@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<RestrictedMenuItem> {
public RestrictedPopupMenuAdapter(Context context, List<RestrictedMenuItem> 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -16,8 +16,6 @@
package com.android.settings.users; package com.android.settings.users;
import static android.os.Process.myUserHandle;
import android.app.Activity; import android.app.Activity;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.Dialog; import android.app.Dialog;
@@ -39,10 +37,12 @@ import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.Process;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.UserHandle; import android.os.UserHandle;
import android.os.UserManager; import android.os.UserManager;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.Menu; import android.view.Menu;
@@ -73,6 +73,8 @@ import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedPreference; import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.drawable.CircleFramedDrawable; import com.android.settingslib.drawable.CircleFramedDrawable;
import com.android.settingslib.search.SearchIndexable; 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.android.settingslib.utils.ThreadUtils;
import com.google.android.setupcompat.util.WizardManagerHelper; import com.google.android.setupcompat.util.WizardManagerHelper;
@@ -83,7 +85,6 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Random;
/** /**
* Screen that manages the list of users on the device. * Screen that manages the list of users on the device.
@@ -165,9 +166,11 @@ public class UserSettings extends SettingsPreferenceFragment
private static SparseArray<Bitmap> sDarkDefaultUserBitmapCache = new SparseArray<>(); private static SparseArray<Bitmap> sDarkDefaultUserBitmapCache = new SparseArray<>();
private MultiUserSwitchBarController mSwitchBarController; private MultiUserSwitchBarController mSwitchBarController;
private EditUserInfoController mEditUserInfoController = new EditUserInfoController(); private EditUserInfoController mEditUserInfoController =
new EditUserInfoController(Utils.FILE_PROVIDER_AUTHORITY);
private AddUserWhenLockedPreferenceController mAddUserWhenLockedPreferenceController; private AddUserWhenLockedPreferenceController mAddUserWhenLockedPreferenceController;
private MultiUserFooterPreferenceController mMultiUserFooterPreferenceController; private MultiUserFooterPreferenceController mMultiUserFooterPreferenceController;
private UserCreatingDialog mUserCreatingDialog;
private CharSequence mPendingUserName; private CharSequence mPendingUserName;
private Drawable mPendingUserIcon; private Drawable mPendingUserIcon;
@@ -175,6 +178,8 @@ public class UserSettings extends SettingsPreferenceFragment
// A place to cache the generated default avatar // A place to cache the generated default avatar
private Drawable mDefaultIconDrawable; 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() { private Handler mHandler = new Handler() {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
@@ -321,9 +326,9 @@ public class UserSettings extends SettingsPreferenceFragment
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mEditUserInfoController.onSaveInstanceState(outState); mEditUserInfoController.onSaveInstanceState(outState);
outState.putInt(SAVE_REMOVING_USER, mRemovingUserId); outState.putInt(SAVE_REMOVING_USER, mRemovingUserId);
super.onSaveInstanceState(outState);
} }
@Override @Override
@@ -471,11 +476,22 @@ public class UserSettings extends SettingsPreferenceFragment
} }
private void onUserCreated(int userId) { private void onUserCreated(int userId) {
hideUserCreatingDialog();
// prevent crash when config changes during user creation
if (getContext() == null) {
return;
}
mAddingUser = false; mAddingUser = false;
UserInfo userInfo = mUserManager.getUserInfo(userId); UserInfo userInfo = mUserManager.getUserInfo(userId);
openUserDetails(userInfo, true); openUserDetails(userInfo, true);
} }
private void hideUserCreatingDialog() {
if (mUserCreatingDialog != null && mUserCreatingDialog.isShowing()) {
mUserCreatingDialog.dismiss();
}
}
private void openUserDetails(UserInfo userInfo, boolean newUser) { private void openUserDetails(UserInfo userInfo, boolean newUser) {
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putInt(UserDetailsSettings.EXTRA_USER_ID, userInfo.id); extras.putInt(UserDetailsSettings.EXTRA_USER_ID, userInfo.id);
@@ -605,94 +621,82 @@ public class UserSettings extends SettingsPreferenceFragment
return dlg; return dlg;
} }
case DIALOG_USER_PROFILE_EDITOR: { case DIALOG_USER_PROFILE_EDITOR: {
UserHandle user = myUserHandle(); return buildEditCurrentUserDialog();
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);
} }
case DIALOG_USER_PROFILE_EDITOR_ADD_USER: { case DIALOG_USER_PROFILE_EDITOR_ADD_USER: {
synchronized (mUserLock) { synchronized (mUserLock) {
mPendingUserIcon = UserIcons.getDefaultUserIcon(getPrefContext().getResources(),
new Random(System.currentTimeMillis()).nextInt(8), false);
mPendingUserName = getString( mPendingUserName = getString(
com.android.settingslib.R.string.user_new_user_name); 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: { case DIALOG_USER_PROFILE_EDITOR_ADD_RESTRICTED_PROFILE: {
synchronized (mUserLock) { synchronized (mUserLock) {
mPendingUserIcon = UserIcons.getDefaultUserIcon(getPrefContext().getResources(),
new Random(System.currentTimeMillis()).nextInt(8), false);
mPendingUserName = getString( mPendingUserName = getString(
com.android.settingslib.R.string.user_new_profile_name); com.android.settingslib.R.string.user_new_profile_name);
mPendingUserIcon = null;
} }
return buildAddUserProfileEditorDialog(USER_TYPE_RESTRICTED_PROFILE); return buildAddUserDialog(USER_TYPE_RESTRICTED_PROFILE);
} }
default: default:
return null; 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; Dialog d;
synchronized (mUserLock) { synchronized (mUserLock) {
d = mEditUserInfoController.createDialog( d = mEditUserInfoController.createDialog(
this, getActivity(),
mPendingUserIcon, this::startActivityForResult,
mPendingUserName, null,
mPendingUserName.toString(),
getString(userType == USER_TYPE_USER getString(userType == USER_TYPE_USER
? com.android.settingslib.R.string.user_info_settings_title ? com.android.settingslib.R.string.user_info_settings_title
: com.android.settingslib.R.string.profile_info_settings_title), : com.android.settingslib.R.string.profile_info_settings_title),
new EditUserInfoController.OnContentChangedCallback() { (userName, userIcon) -> {
@Override mPendingUserIcon = userIcon;
public void onPhotoChanged(UserHandle user, Drawable photo) { mPendingUserName = userName;
mPendingUserIcon = photo; addUserNow(userType);
}
@Override
public void onLabelChanged(UserHandle user, CharSequence label) {
mPendingUserName = label;
}
}, },
myUserHandle(), () -> {
new EditUserInfoController.OnDialogCompleteCallback() { synchronized (mUserLock) {
@Override mPendingUserIcon = null;
public void onPositive() { mPendingUserName = null;
addUserNow(userType);
} }
}
@Override );
public void onNegativeOrCancel() {
synchronized (mUserLock) {
mPendingUserIcon = null;
mPendingUserName = null;
}
}
});
} }
return d; return d;
} }
@@ -759,6 +763,9 @@ public class UserSettings extends SettingsPreferenceFragment
: (mPendingUserName != null ? mPendingUserName.toString() : (mPendingUserName != null ? mPendingUserName.toString()
: getString(R.string.user_new_profile_name)); : getString(R.string.user_new_profile_name));
} }
mUserCreatingDialog = new UserCreatingDialog(getActivity());
mUserCreatingDialog.show();
ThreadUtils.postOnBackgroundThread(new Runnable() { ThreadUtils.postOnBackgroundThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -781,13 +788,15 @@ public class UserSettings extends SettingsPreferenceFragment
mAddingUser = false; mAddingUser = false;
mPendingUserIcon = null; mPendingUserIcon = null;
mPendingUserName = null; mPendingUserName = null;
ThreadUtils.postOnMainThread(() -> hideUserCreatingDialog());
return; return;
} }
if (mPendingUserIcon != null) { Drawable newUserIcon = mPendingUserIcon;
mUserManager.setUserIcon(user.id, if (newUserIcon == null) {
UserIcons.convertToBitmap(mPendingUserIcon)); newUserIcon = UserIcons.getDefaultUserIcon(getResources(), user.id, false);
} }
mUserManager.setUserIcon(user.id, UserIcons.convertToBitmap(newUserIcon));
if (userType == USER_TYPE_USER) { if (userType == USER_TYPE_USER) {
mHandler.sendEmptyMessage(MESSAGE_UPDATE_LIST); mHandler.sendEmptyMessage(MESSAGE_UPDATE_LIST);

View File

@@ -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();
}
}