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