From f98ae93fdf751e5b2326b1f78599a87e0be028a7 Mon Sep 17 00:00:00 2001 From: Oli Lan Date: Mon, 17 Jan 2022 18:19:36 +0000 Subject: [PATCH] Add avatar picker that shows a predefined set of images. This is the first in a series of CLs to add a new avatar picker. This CL adds the basic avatar picker screen to show a set of predefined images (that can be supplied as an array of drawables). Resources are provided to adapt the screen to phones and tablets of various sizes. Future CLs will integrate the take photo and choose photo functionality from the current dialog, and will also add a set of default icons that can be chosen if no predefined images have been provided. Bug: 215134398 Test: manual check screen on various screen sizes Change-Id: I6ce95549cc17dc5a9b67d9dc43c5b7d2bda37874 --- packages/SettingsLib/Android.bp | 1 + packages/SettingsLib/AndroidManifest.xml | 6 + .../res/drawable/avatar_selector.xml | 23 +++ .../res/drawable/ic_account_circle.xml | 25 +++ .../SettingsLib/res/layout/avatar_item.xml | 24 +++ .../SettingsLib/res/layout/avatar_picker.xml | 38 ++++ .../res/values-w1280dp-land/dimens.xml | 21 ++ .../res/values-w1440dp-land/dimens.xml | 21 ++ .../res/values-w1600dp-land/dimens.xml | 21 ++ .../res/values-w480dp-port/dimens.xml | 21 ++ .../res/values-w600dp-port/dimens.xml | 21 ++ .../res/values-w720dp-port/dimens.xml | 21 ++ .../res/values-w840dp-port/dimens.xml | 21 ++ .../res/values-w960dp-land/dimens.xml | 21 ++ packages/SettingsLib/res/values/arrays.xml | 2 + packages/SettingsLib/res/values/dimens.xml | 5 + packages/SettingsLib/res/values/strings.xml | 3 + .../users/AvatarPickerActivity.java | 194 ++++++++++++++++++ .../users/EditUserInfoController.java | 34 ++- .../users/EditUserPhotoController.java | 23 ++- .../users/EditUserInfoControllerTest.java | 10 +- 21 files changed, 541 insertions(+), 15 deletions(-) create mode 100644 packages/SettingsLib/res/drawable/avatar_selector.xml create mode 100644 packages/SettingsLib/res/drawable/ic_account_circle.xml create mode 100644 packages/SettingsLib/res/layout/avatar_item.xml create mode 100644 packages/SettingsLib/res/layout/avatar_picker.xml create mode 100644 packages/SettingsLib/res/values-w1280dp-land/dimens.xml create mode 100644 packages/SettingsLib/res/values-w1440dp-land/dimens.xml create mode 100644 packages/SettingsLib/res/values-w1600dp-land/dimens.xml create mode 100644 packages/SettingsLib/res/values-w480dp-port/dimens.xml create mode 100644 packages/SettingsLib/res/values-w600dp-port/dimens.xml create mode 100644 packages/SettingsLib/res/values-w720dp-port/dimens.xml create mode 100644 packages/SettingsLib/res/values-w840dp-port/dimens.xml create mode 100644 packages/SettingsLib/res/values-w960dp-land/dimens.xml create mode 100644 packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index fcf2282160a7f..684f4dee9ac2c 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -50,6 +50,7 @@ android_library { "SettingsLibSettingsTransition", "SettingsLibActivityEmbedding", "SettingsLibButtonPreference", + "setupdesign", ], // ANDROIDMK TRANSLATION ERROR: unsupported assignment to LOCAL_SHARED_JAVA_LIBRARIES diff --git a/packages/SettingsLib/AndroidManifest.xml b/packages/SettingsLib/AndroidManifest.xml index a3473459948d8..13f8a372c9b57 100644 --- a/packages/SettingsLib/AndroidManifest.xml +++ b/packages/SettingsLib/AndroidManifest.xml @@ -18,4 +18,10 @@ + + + + diff --git a/packages/SettingsLib/res/drawable/avatar_selector.xml b/packages/SettingsLib/res/drawable/avatar_selector.xml new file mode 100644 index 0000000000000..b4eea209780b1 --- /dev/null +++ b/packages/SettingsLib/res/drawable/avatar_selector.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ 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.xml new file mode 100644 index 0000000000000..0cc54b6b49728 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_account_circle.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/SettingsLib/res/layout/avatar_item.xml b/packages/SettingsLib/res/layout/avatar_item.xml new file mode 100644 index 0000000000000..c52f6648cb7ee --- /dev/null +++ b/packages/SettingsLib/res/layout/avatar_item.xml @@ -0,0 +1,24 @@ + + + diff --git a/packages/SettingsLib/res/layout/avatar_picker.xml b/packages/SettingsLib/res/layout/avatar_picker.xml new file mode 100644 index 0000000000000..ffb32f70b14ed --- /dev/null +++ b/packages/SettingsLib/res/layout/avatar_picker.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/SettingsLib/res/values-w1280dp-land/dimens.xml b/packages/SettingsLib/res/values-w1280dp-land/dimens.xml new file mode 100644 index 0000000000000..4097d05642512 --- /dev/null +++ b/packages/SettingsLib/res/values-w1280dp-land/dimens.xml @@ -0,0 +1,21 @@ + + + 4 + 104dp + 8dp + 3dp + \ No newline at end of file diff --git a/packages/SettingsLib/res/values-w1440dp-land/dimens.xml b/packages/SettingsLib/res/values-w1440dp-land/dimens.xml new file mode 100644 index 0000000000000..764870efe2228 --- /dev/null +++ b/packages/SettingsLib/res/values-w1440dp-land/dimens.xml @@ -0,0 +1,21 @@ + + + 4 + 128dp + 8dp + 4dp + \ No newline at end of file diff --git a/packages/SettingsLib/res/values-w1600dp-land/dimens.xml b/packages/SettingsLib/res/values-w1600dp-land/dimens.xml new file mode 100644 index 0000000000000..872b88a3776d8 --- /dev/null +++ b/packages/SettingsLib/res/values-w1600dp-land/dimens.xml @@ -0,0 +1,21 @@ + + + 4 + 148dp + 8dp + 6dp + \ No newline at end of file diff --git a/packages/SettingsLib/res/values-w480dp-port/dimens.xml b/packages/SettingsLib/res/values-w480dp-port/dimens.xml new file mode 100644 index 0000000000000..cab78d64ce8df --- /dev/null +++ b/packages/SettingsLib/res/values-w480dp-port/dimens.xml @@ -0,0 +1,21 @@ + + + 3 + 112dp + 8dp + 3dp + diff --git a/packages/SettingsLib/res/values-w600dp-port/dimens.xml b/packages/SettingsLib/res/values-w600dp-port/dimens.xml new file mode 100644 index 0000000000000..4097d05642512 --- /dev/null +++ b/packages/SettingsLib/res/values-w600dp-port/dimens.xml @@ -0,0 +1,21 @@ + + + 4 + 104dp + 8dp + 3dp + \ No newline at end of file diff --git a/packages/SettingsLib/res/values-w720dp-port/dimens.xml b/packages/SettingsLib/res/values-w720dp-port/dimens.xml new file mode 100644 index 0000000000000..764870efe2228 --- /dev/null +++ b/packages/SettingsLib/res/values-w720dp-port/dimens.xml @@ -0,0 +1,21 @@ + + + 4 + 128dp + 8dp + 4dp + \ No newline at end of file diff --git a/packages/SettingsLib/res/values-w840dp-port/dimens.xml b/packages/SettingsLib/res/values-w840dp-port/dimens.xml new file mode 100644 index 0000000000000..872b88a3776d8 --- /dev/null +++ b/packages/SettingsLib/res/values-w840dp-port/dimens.xml @@ -0,0 +1,21 @@ + + + 4 + 148dp + 8dp + 6dp + \ No newline at end of file diff --git a/packages/SettingsLib/res/values-w960dp-land/dimens.xml b/packages/SettingsLib/res/values-w960dp-land/dimens.xml new file mode 100644 index 0000000000000..8403dba543292 --- /dev/null +++ b/packages/SettingsLib/res/values-w960dp-land/dimens.xml @@ -0,0 +1,21 @@ + + + 4 + 96dp + 6dp + 2dp + diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml index 2b5e9cdc017d0..93e3deef7aa52 100644 --- a/packages/SettingsLib/res/values/arrays.xml +++ b/packages/SettingsLib/res/values/arrays.xml @@ -647,4 +647,6 @@ disabled + + diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml index 9ee42b648d3df..e46d69e135d68 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -102,4 +102,9 @@ 112dp 32dp + 3 + 96dp + 6dp + 2dp + diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 5ada02825b31c..45f8f1debe340 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1548,4 +1548,7 @@ No calling. + + + Choose a profile picture diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java new file mode 100644 index 0000000000000..4cd107df1d090 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2022 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.settingslib.users; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.core.graphics.drawable.RoundedBitmapDrawable; +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settingslib.R; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; +import com.google.android.setupdesign.util.ThemeHelper; + +/** + * Activity to allow the user to choose a user profile picture. + */ +public class AvatarPickerActivity extends Activity { + + static final String EXTRA_AVATAR_INDEX = "avatar_index"; + + private FooterButton mDoneButton; + private AvatarAdapter mAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.trySetDynamicColor(this); + setContentView(R.layout.avatar_picker); + + GlifLayout glifLayout = findViewById(R.id.glif_layout); + RecyclerView recyclerView = findViewById(R.id.avatar_grid); + FooterBarMixin mixin = glifLayout.getMixin(FooterBarMixin.class); + + FooterButton secondaryButton = + new FooterButton.Builder(this) + .setText("Cancel") + .setListener(view -> cancel()) + .build(); + + mDoneButton = + new FooterButton.Builder(this) + .setText("Done") + .setListener(view -> confirmSelection()) + .build(); + mDoneButton.setEnabled(false); + + mixin.setSecondaryButton(secondaryButton); + mixin.setPrimaryButton(mDoneButton); + + mAdapter = new AvatarAdapter(); + recyclerView.setAdapter(mAdapter); + recyclerView.setLayoutManager(new GridLayoutManager(this, + getResources().getInteger(R.integer.avatar_picker_columns))); + } + + private void confirmSelection() { + Intent data = new Intent(); + data.putExtra(EXTRA_AVATAR_INDEX, mAdapter.indexFromPosition(mAdapter.mSelectedPosition)); + setResult(RESULT_OK, data); + finish(); + } + + private void cancel() { + setResult(RESULT_CANCELED); + finish(); + } + + private class AvatarAdapter extends RecyclerView.Adapter { + + private static final int NONE = -1; + private static final int AVATAR_START_POSITION = 0; + + private final TypedArray mImageDrawables; + private int mSelectedPosition = NONE; + + AvatarAdapter() { + mImageDrawables = getResources().obtainTypedArray(R.array.avatar_images); + } + + @NonNull + @Override + public AvatarViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { + LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + View itemView = layoutInflater.inflate(R.layout.avatar_item, parent, false); + return new AvatarViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull AvatarViewHolder viewHolder, int position) { + 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.setClickListener(view -> { + if (mSelectedPosition == position) { + deselect(position); + } else { + select(position); + } + }); + } + + @Override + public int getItemCount() { + return AVATAR_START_POSITION + mImageDrawables.length(); + } + + private Drawable circularDrawableFrom(BitmapDrawable drawable) { + Bitmap bitmap = drawable.getBitmap(); + + RoundedBitmapDrawable roundedBitmapDrawable = + RoundedBitmapDrawableFactory.create(getResources(), bitmap); + roundedBitmapDrawable.setCircular(true); + + return roundedBitmapDrawable; + } + + private int indexFromPosition(int position) { + return position - AVATAR_START_POSITION; + } + + private void select(int position) { + final int oldSelection = mSelectedPosition; + mSelectedPosition = position; + notifyItemChanged(position); + if (oldSelection != NONE) { + notifyItemChanged(oldSelection); + } else { + mDoneButton.setEnabled(true); + } + } + + private void deselect(int position) { + mSelectedPosition = NONE; + notifyItemChanged(position); + mDoneButton.setEnabled(false); + } + } + + private static class AvatarViewHolder extends RecyclerView.ViewHolder { + private final ImageView mImageView; + + AvatarViewHolder(View view) { + super(view); + mImageView = view.findViewById(R.id.avatar_image); + } + + public void setDrawable(Drawable drawable) { + mImageView.setImageDrawable(drawable); + } + + public void setClickListener(View.OnClickListener listener) { + mImageView.setOnClickListener(listener); + } + + public void setSelected(boolean selected) { + mImageView.setSelected(selected); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java index 62043363d85f7..fb0346dacb53a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java @@ -25,6 +25,7 @@ import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; +import android.os.UserManager; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; @@ -36,6 +37,8 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.UserIcons; import com.android.settingslib.R; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.drawable.CircleFramedDrawable; import java.io.File; @@ -139,12 +142,20 @@ public class EditUserInfoController { Drawable userIcon = getUserIcon(activity, defaultUserIcon); userPhotoView.setImageDrawable(userIcon); - if (canChangePhoto(activity)) { - mEditUserPhotoController = createEditUserPhotoController(activity, activityStarter, - userPhotoView); - } else { - // some users can't change their photos, so we need to remove the suggestive icon + if (isChangePhotoRestrictedByBase(activity)) { + // some users can't change their photos so we need to remove the suggestive icon content.findViewById(R.id.add_a_photo_icon).setVisibility(View.GONE); + } else { + RestrictedLockUtils.EnforcedAdmin adminRestriction = + getChangePhotoAdminRestriction(activity); + if (adminRestriction != null) { + userPhotoView.setOnClickListener(view -> + RestrictedLockUtils.sendShowAdminSupportDetailsIntent( + activity, adminRestriction)); + } else { + mEditUserPhotoController = createEditUserPhotoController(activity, activityStarter, + userPhotoView); + } } mEditUserInfoDialog = buildDialog(activity, content, userNameView, oldUserIcon, @@ -203,10 +214,15 @@ public class EditUserInfoController { } @VisibleForTesting - boolean canChangePhoto(Context context) { - return (PhotoCapabilityUtils.canCropPhoto(context) - && PhotoCapabilityUtils.canChoosePhoto(context)) - || PhotoCapabilityUtils.canTakePhoto(context); + boolean isChangePhotoRestrictedByBase(Context context) { + return RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId()); + } + + @VisibleForTesting + RestrictedLockUtils.EnforcedAdmin getChangePhotoAdminRestriction(Context context) { + return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, UserManager.DISALLOW_SET_USER_ICON, UserHandle.myUserId()); } @VisibleForTesting diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java index f9584a3e15e9a..ee4c9fde9b6e5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java @@ -21,6 +21,8 @@ import android.content.ClipData; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; +import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; @@ -29,6 +31,7 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.net.Uri; @@ -78,6 +81,7 @@ public class EditUserPhotoController { 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 int REQUEST_CODE_PICK_AVATAR = 1004; // in rare cases we get a null Cursor when querying for DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI // so we need a default photo size private static final int DEFAULT_PHOTO_SIZE = 500; @@ -113,7 +117,7 @@ public class EditUserPhotoController { mCropPictureUri = createTempImageUri(activity, CROP_PICTURE_FILE_NAME, !waiting); mTakePictureUri = createTempImageUri(activity, TAKE_PICTURE_FILE_NAME, !waiting); mPhotoSize = getPhotoSize(activity); - mImageView.setOnClickListener(v -> showUpdatePhotoPopup()); + mImageView.setOnClickListener(v -> showAvatarPicker()); mNewUserPhotoBitmap = bitmap; } @@ -151,6 +155,18 @@ public class EditUserPhotoController { copyAndCropPhoto(pictureUri); } return true; + case REQUEST_CODE_PICK_AVATAR: + int index = data.getIntExtra(AvatarPickerActivity.EXTRA_AVATAR_INDEX, -1); + Resources res = mImageView.getContext().getResources(); + try (TypedArray avatarImages = res.obtainTypedArray(R.array.avatar_images)) { + if (index >= 0 && index < avatarImages.length()) { + Drawable drawable = avatarImages.getDrawable(index); + if (!(drawable instanceof BitmapDrawable)) { + return false; + } + onPhotoProcessed(((BitmapDrawable) drawable).getBitmap()); + } + } } return false; } @@ -159,6 +175,11 @@ public class EditUserPhotoController { return mNewUserPhotoDrawable; } + private void showAvatarPicker() { + Intent intent = new Intent(mImageView.getContext(), AvatarPickerActivity.class); + mActivityStarter.startActivityForResult(intent, REQUEST_CODE_PICK_AVATAR); + } + private void showUpdatePhotoPopup() { final Context context = mImageView.getContext(); final boolean canTakePhoto = PhotoCapabilityUtils.canTakePhoto(context); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java index d6c8816ecc585..a5ee4c35f7244 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java @@ -62,7 +62,7 @@ public class EditUserInfoControllerTest { @Mock private ActivityStarter mActivityStarter; - private boolean mCanChangePhoto; + private boolean mPhotoRestrictedByBase; private Activity mActivity; private TestEditUserInfoController mController; @@ -85,8 +85,8 @@ public class EditUserInfoControllerTest { } @Override - boolean canChangePhoto(Context context) { - return mCanChangePhoto; + boolean isChangePhotoRestrictedByBase(Context context) { + return mPhotoRestrictedByBase; } } @@ -96,7 +96,7 @@ public class EditUserInfoControllerTest { mActivity = spy(ActivityController.of(new FragmentActivity()).get()); mActivity.setTheme(R.style.Theme_AppCompat_DayNight); mController = new TestEditUserInfoController(); - mCanChangePhoto = true; + mPhotoRestrictedByBase = true; } @Test @@ -260,7 +260,7 @@ public class EditUserInfoControllerTest { @Test public void createDialog_canNotChangePhoto_nullPhotoController() { - mCanChangePhoto = false; + mPhotoRestrictedByBase = false; mController.createDialog(mActivity, mActivityStarter, mCurrentIcon, "test", "title", null, null);