Offer colored default avatars if no preselected images are provided.
In the new avatar picker, if no preselected images have been provided, this CL adds several color variations of the default avatar to the picker instead. Bug: 215134398 Test: manual Change-Id: I3e1f617c760b241517f94c606f5cb21eafcc022d
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.internal.util;
|
||||
|
||||
import android.annotation.ColorInt;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
@@ -72,9 +73,30 @@ public class UserIcons {
|
||||
// Return colored icon instead
|
||||
colorResId = USER_ICON_COLORS[userId % USER_ICON_COLORS.length];
|
||||
}
|
||||
return getDefaultUserIconInColor(resources, resources.getColor(colorResId, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a default user icon in a particular color.
|
||||
*
|
||||
* @param resources resources object to fetch the user icon
|
||||
* @param color the color used for the icon
|
||||
*/
|
||||
public static Drawable getDefaultUserIconInColor(Resources resources, @ColorInt int color) {
|
||||
Drawable icon = resources.getDrawable(R.drawable.ic_account_circle, null).mutate();
|
||||
icon.setColorFilter(resources.getColor(colorResId, null), Mode.SRC_IN);
|
||||
icon.setColorFilter(color, Mode.SRC_IN);
|
||||
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array containing colors to be used for default user icons.
|
||||
*/
|
||||
public static int[] getUserIconColors(Resources resources) {
|
||||
int[] result = new int[USER_ICON_COLORS.length];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = resources.getColor(USER_ICON_COLORS[i], null);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?android:attr/colorPrimary"/>
|
||||
<stroke
|
||||
android:color="?android:attr/colorPrimary"
|
||||
android:width="@dimen/avatar_picker_padding"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -20,7 +20,7 @@
|
||||
android:id="@+id/glif_layout"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:icon="@drawable/ic_account_circle"
|
||||
android:icon="@drawable/ic_account_circle_outline"
|
||||
app:sucUsePartnerResource="true"
|
||||
app:sucHeaderText="@string/avatar_picker_title">
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class AvatarPhotoController {
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_CROP_PHOTO:
|
||||
mActivity.returnResult(pictureUri);
|
||||
mActivity.returnUriResult(pictureUri);
|
||||
return true;
|
||||
case REQUEST_CODE_TAKE_PHOTO:
|
||||
case REQUEST_CODE_CHOOSE_PHOTO:
|
||||
@@ -232,7 +232,7 @@ class AvatarPhotoController {
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
saveBitmapToFile(bitmap, new File(mImagesDir, CROP_PICTURE_FILE_NAME));
|
||||
mActivity.returnResult(mCropPictureUri);
|
||||
mActivity.returnUriResult(mCropPictureUri);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.internal.util.UserIcons;
|
||||
import com.android.settingslib.R;
|
||||
|
||||
import com.google.android.setupcompat.template.FooterBarMixin;
|
||||
@@ -43,12 +44,29 @@ import com.google.android.setupcompat.template.FooterButton;
|
||||
import com.google.android.setupdesign.GlifLayout;
|
||||
import com.google.android.setupdesign.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Activity to allow the user to choose a user profile picture.
|
||||
*
|
||||
* <p>Options are provided to take a photo or choose a photo using the photo picker. In addition,
|
||||
* preselected avatar images may be provided in the resource array {@code avatar_images}. If
|
||||
* provided, every element of that array must be a bitmap drawable.
|
||||
*
|
||||
* <p>If preselected images are not provided, the default avatar will be shown instead, in a range
|
||||
* of colors.
|
||||
*
|
||||
* <p>This activity should be started with startActivityForResult. If a photo or a preselected image
|
||||
* is selected, a Uri will be returned in the data field of the result intent. If a colored default
|
||||
* avatar is selected, the chosen color will be returned as {@code EXTRA_DEFAULT_ICON_TINT_COLOR}
|
||||
* and the data field will be empty.
|
||||
*/
|
||||
public class AvatarPickerActivity extends Activity {
|
||||
|
||||
static final String EXTRA_FILE_AUTHORITY = "file_authority";
|
||||
static final String EXTRA_DEFAULT_ICON_TINT_COLOR = "default_icon_tint_color";
|
||||
|
||||
private static final String KEY_AWAITING_RESULT = "awaiting_result";
|
||||
private static final String KEY_SELECTED_POSITION = "selected_position";
|
||||
|
||||
@@ -91,7 +109,7 @@ public class AvatarPickerActivity extends Activity {
|
||||
mDoneButton =
|
||||
new FooterButton.Builder(this)
|
||||
.setText("Done")
|
||||
.setListener(view -> returnResult(mAdapter.uriForSelection()))
|
||||
.setListener(view -> mAdapter.returnSelectionResult())
|
||||
.build();
|
||||
mDoneButton.setEnabled(false);
|
||||
|
||||
@@ -134,13 +152,20 @@ public class AvatarPickerActivity extends Activity {
|
||||
super.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
void returnResult(Uri uri) {
|
||||
void returnUriResult(Uri uri) {
|
||||
Intent resultData = new Intent();
|
||||
resultData.setData(uri);
|
||||
setResult(RESULT_OK, resultData);
|
||||
finish();
|
||||
}
|
||||
|
||||
void returnColorResult(int color) {
|
||||
Intent resultData = new Intent();
|
||||
resultData.putExtra(EXTRA_DEFAULT_ICON_TINT_COLOR, color);
|
||||
setResult(RESULT_OK, resultData);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void cancel() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
@@ -154,7 +179,9 @@ public class AvatarPickerActivity extends Activity {
|
||||
private final int mChoosePhotoPosition;
|
||||
private final int mPreselectedImageStartPosition;
|
||||
|
||||
private final TypedArray mImageDrawables;
|
||||
private final List<Drawable> mImageDrawables;
|
||||
private final TypedArray mPreselectedImages;
|
||||
private final int[] mUserIconColors;
|
||||
private int mSelectedPosition = NONE;
|
||||
|
||||
AvatarAdapter() {
|
||||
@@ -166,7 +193,9 @@ public class AvatarPickerActivity extends Activity {
|
||||
mChoosePhotoPosition = (canChoosePhoto ? (canTakePhoto ? 1 : 0) : NONE);
|
||||
mPreselectedImageStartPosition = (canTakePhoto ? 1 : 0) + (canChoosePhoto ? 1 : 0);
|
||||
|
||||
mImageDrawables = getResources().obtainTypedArray(R.array.avatar_images);
|
||||
mPreselectedImages = getResources().obtainTypedArray(R.array.avatar_images);
|
||||
mUserIconColors = UserIcons.getUserIconColors(getResources());
|
||||
mImageDrawables = buildDrawableList();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -188,14 +217,8 @@ public class AvatarPickerActivity extends Activity {
|
||||
viewHolder.setClickListener(view -> mAvatarPhotoController.choosePhoto());
|
||||
|
||||
} else if (position >= mPreselectedImageStartPosition) {
|
||||
Drawable drawable = mImageDrawables.getDrawable(indexFromPosition(position));
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
drawable = circularDrawableFrom((BitmapDrawable) drawable);
|
||||
} else {
|
||||
throw new IllegalStateException("Avatar drawables must be bitmaps");
|
||||
}
|
||||
viewHolder.setSelected(position == mSelectedPosition);
|
||||
viewHolder.setDrawable(drawable);
|
||||
viewHolder.setDrawable(mImageDrawables.get(indexFromPosition(position)));
|
||||
viewHolder.setClickListener(view -> {
|
||||
if (mSelectedPosition == position) {
|
||||
deselect(position);
|
||||
@@ -208,7 +231,29 @@ public class AvatarPickerActivity extends Activity {
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mPreselectedImageStartPosition + mImageDrawables.length();
|
||||
return mPreselectedImageStartPosition + mImageDrawables.size();
|
||||
}
|
||||
|
||||
private List<Drawable> buildDrawableList() {
|
||||
List<Drawable> result = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < mPreselectedImages.length(); i++) {
|
||||
Drawable drawable = mPreselectedImages.getDrawable(i);
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
result.add(circularDrawableFrom((BitmapDrawable) drawable));
|
||||
} else {
|
||||
throw new IllegalStateException("Avatar drawables must be bitmaps");
|
||||
}
|
||||
}
|
||||
if (!result.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// No preselected images. Use tinted default icon.
|
||||
for (int i = 0; i < mUserIconColors.length; i++) {
|
||||
result.add(UserIcons.getDefaultUserIconInColor(getResources(), mUserIconColors[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Drawable circularDrawableFrom(BitmapDrawable drawable) {
|
||||
@@ -242,13 +287,18 @@ public class AvatarPickerActivity extends Activity {
|
||||
mDoneButton.setEnabled(false);
|
||||
}
|
||||
|
||||
private Uri uriForSelection() {
|
||||
int resourceId =
|
||||
mImageDrawables.getResourceId(indexFromPosition(mSelectedPosition), -1);
|
||||
if (resourceId == -1) {
|
||||
throw new IllegalStateException("Preselected avatar images must be resources.");
|
||||
private void returnSelectionResult() {
|
||||
int index = indexFromPosition(mSelectedPosition);
|
||||
if (mPreselectedImages.length() > 0) {
|
||||
int resourceId = mPreselectedImages.getResourceId(index, -1);
|
||||
if (resourceId == -1) {
|
||||
throw new IllegalStateException("Preselected avatar images must be resources.");
|
||||
}
|
||||
returnUriResult(uriForResourceId(resourceId));
|
||||
} else {
|
||||
returnColorResult(
|
||||
mUserIconColors[index]);
|
||||
}
|
||||
return uriForResourceId(resourceId);
|
||||
}
|
||||
|
||||
private Uri uriForResourceId(int resourceId) {
|
||||
|
||||
@@ -16,17 +16,21 @@
|
||||
|
||||
package com.android.settingslib.users;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.android.internal.util.UserIcons;
|
||||
import com.android.settingslib.R;
|
||||
import com.android.settingslib.drawable.CircleFramedDrawable;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -34,6 +38,7 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* This class contains logic for starting activities to take/choose/crop photo, reads and transforms
|
||||
@@ -81,10 +86,17 @@ public class EditUserPhotoController {
|
||||
}
|
||||
|
||||
if (requestCode == REQUEST_CODE_PICK_AVATAR) {
|
||||
if (data.hasExtra(AvatarPickerActivity.EXTRA_DEFAULT_ICON_TINT_COLOR)) {
|
||||
int tintColor =
|
||||
data.getIntExtra(AvatarPickerActivity.EXTRA_DEFAULT_ICON_TINT_COLOR, -1);
|
||||
onDefaultIconSelected(tintColor);
|
||||
return true;
|
||||
}
|
||||
if (data.getData() != null) {
|
||||
onPhotoCropped(data.getData());
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -99,36 +111,54 @@ public class EditUserPhotoController {
|
||||
mActivityStarter.startActivityForResult(intent, REQUEST_CODE_PICK_AVATAR);
|
||||
}
|
||||
|
||||
private void onDefaultIconSelected(int tintColor) {
|
||||
try {
|
||||
ThreadUtils.postOnBackgroundThread(() -> {
|
||||
Drawable drawable =
|
||||
UserIcons.getDefaultUserIconInColor(mActivity.getResources(), tintColor);
|
||||
Bitmap bitmap = convertToBitmap(drawable,
|
||||
(int) mActivity.getResources().getDimension(R.dimen.circle_avatar_size));
|
||||
|
||||
ThreadUtils.postOnMainThread(() -> onPhotoProcessed(bitmap));
|
||||
}).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.e(TAG, "Error processing default icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap convertToBitmap(@NonNull Drawable icon, int size) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
icon.setBounds(0, 0, size, size);
|
||||
icon.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private void onPhotoCropped(final Uri data) {
|
||||
// TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change
|
||||
new AsyncTask<Void, Void, Bitmap>() {
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... params) {
|
||||
InputStream imageStream = null;
|
||||
try {
|
||||
imageStream = mActivity.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);
|
||||
}
|
||||
ThreadUtils.postOnBackgroundThread(() -> {
|
||||
InputStream imageStream = null;
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
imageStream = mActivity.getContentResolver()
|
||||
.openInputStream(data);
|
||||
bitmap = BitmapFactory.decodeStream(imageStream);
|
||||
} catch (FileNotFoundException fe) {
|
||||
Log.w(TAG, "Cannot find image file", fe);
|
||||
} finally {
|
||||
if (imageStream != null) {
|
||||
try {
|
||||
imageStream.close();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Cannot close image stream", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
onPhotoProcessed(bitmap);
|
||||
|
||||
if (bitmap != null) {
|
||||
Bitmap finalBitmap = bitmap;
|
||||
ThreadUtils.postOnMainThread(() -> onPhotoProcessed(finalBitmap));
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
|
||||
});
|
||||
}
|
||||
|
||||
private void onPhotoProcessed(Bitmap bitmap) {
|
||||
|
||||
Reference in New Issue
Block a user