From 4f3a0b8fdbf7f7947345240d53fd6dcfc51371a2 Mon Sep 17 00:00:00 2001 From: Oli Lan Date: Fri, 21 Jan 2022 10:24:34 +0000 Subject: [PATCH] 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 --- .../com/android/internal/util/UserIcons.java | 24 +++++- .../res/drawable/avatar_selector.xml | 4 +- ...rcle.xml => ic_account_circle_outline.xml} | 0 .../SettingsLib/res/layout/avatar_picker.xml | 2 +- .../users/AvatarPhotoController.java | 4 +- .../users/AvatarPickerActivity.java | 86 +++++++++++++++---- .../users/EditUserPhotoController.java | 82 ++++++++++++------ 7 files changed, 153 insertions(+), 49 deletions(-) rename packages/SettingsLib/res/drawable/{ic_account_circle.xml => ic_account_circle_outline.xml} (100%) diff --git a/core/java/com/android/internal/util/UserIcons.java b/core/java/com/android/internal/util/UserIcons.java index bfe43237da58d..17b84ffc2f3fa 100644 --- a/core/java/com/android/internal/util/UserIcons.java +++ b/core/java/com/android/internal/util/UserIcons.java @@ -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; + } } diff --git a/packages/SettingsLib/res/drawable/avatar_selector.xml b/packages/SettingsLib/res/drawable/avatar_selector.xml index b4eea209780b1..ccde59763a4ab 100644 --- a/packages/SettingsLib/res/drawable/avatar_selector.xml +++ b/packages/SettingsLib/res/drawable/avatar_selector.xml @@ -17,7 +17,9 @@ - + \ No newline at end of file diff --git a/packages/SettingsLib/res/drawable/ic_account_circle.xml b/packages/SettingsLib/res/drawable/ic_account_circle_outline.xml similarity index 100% rename from packages/SettingsLib/res/drawable/ic_account_circle.xml rename to packages/SettingsLib/res/drawable/ic_account_circle_outline.xml diff --git a/packages/SettingsLib/res/layout/avatar_picker.xml b/packages/SettingsLib/res/layout/avatar_picker.xml index ffb32f70b14ed..2d40bd0de7eae 100644 --- a/packages/SettingsLib/res/layout/avatar_picker.xml +++ b/packages/SettingsLib/res/layout/avatar_picker.xml @@ -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"> diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java index 8dfd1bd5d05c6..61b8911acea45 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java @@ -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); } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java index 2d9809d2f69ed..50015e6533999 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java @@ -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. + * + *

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. + * + *

If preselected images are not provided, the default avatar will be shown instead, in a range + * of colors. + * + *

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 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 buildDrawableList() { + List 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) { diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java index 03b6d2444f225..f8bb38b5978ec 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java @@ -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() { - @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) {