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) {