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