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/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_choose_photo_circled.xml b/packages/SettingsLib/res/drawable/avatar_choose_photo_circled.xml new file mode 100644 index 0000000000000..97aec740ea435 --- /dev/null +++ b/packages/SettingsLib/res/drawable/avatar_choose_photo_circled.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/packages/SettingsLib/res/drawable/avatar_selector.xml b/packages/SettingsLib/res/drawable/avatar_selector.xml new file mode 100644 index 0000000000000..ccde59763a4ab --- /dev/null +++ b/packages/SettingsLib/res/drawable/avatar_selector.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/SettingsLib/res/drawable/avatar_take_photo_circled.xml b/packages/SettingsLib/res/drawable/avatar_take_photo_circled.xml new file mode 100644 index 0000000000000..7033aaeec9117 --- /dev/null +++ b/packages/SettingsLib/res/drawable/avatar_take_photo_circled.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/packages/SettingsLib/res/drawable/ic_account_circle_outline.xml b/packages/SettingsLib/res/drawable/ic_account_circle_outline.xml new file mode 100644 index 0000000000000..0cc54b6b49728 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_account_circle_outline.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/packages/SettingsLib/res/drawable/ic_avatar_choose_photo.xml b/packages/SettingsLib/res/drawable/ic_avatar_choose_photo.xml new file mode 100644 index 0000000000000..b85fdc2de5970 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_avatar_choose_photo.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/SettingsLib/res/drawable/ic_avatar_take_photo.xml b/packages/SettingsLib/res/drawable/ic_avatar_take_photo.xml new file mode 100644 index 0000000000000..5c56276ec4f34 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_avatar_take_photo.xml @@ -0,0 +1,24 @@ + + + + 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..2d40bd0de7eae --- /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..120df76218b37 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -102,4 +102,11 @@ 112dp 32dp + 3 + 96dp + 6dp + 2dp + + 25dp + 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/AvatarPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java new file mode 100644 index 0000000000000..61b8911acea45 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java @@ -0,0 +1,297 @@ +/* + * 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.ClipData; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.StrictMode; +import android.provider.ContactsContract; +import android.provider.MediaStore; +import android.util.EventLog; +import android.util.Log; + +import androidx.core.content.FileProvider; + +import libcore.io.Streams; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class AvatarPhotoController { + private static final String TAG = "AvatarPhotoController"; + + 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; + // 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; + + private static final String IMAGES_DIR = "multi_user"; + private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; + private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto.jpg"; + + private final int mPhotoSize; + + private final AvatarPickerActivity mActivity; + private final String mFileAuthority; + + private final File mImagesDir; + private final Uri mCropPictureUri; + private final Uri mTakePictureUri; + + AvatarPhotoController(AvatarPickerActivity activity, boolean waiting, String fileAuthority) { + mActivity = activity; + mFileAuthority = fileAuthority; + + mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR); + mImagesDir.mkdir(); + mCropPictureUri = createTempImageUri(activity, CROP_PICTURE_FILE_NAME, !waiting); + mTakePictureUri = createTempImageUri(activity, TAKE_PICTURE_FILE_NAME, !waiting); + mPhotoSize = getPhotoSize(activity); + } + + /** + * Handles activity result from containing activity/fragment after a take/choose/crop photo + * action result is received. + */ + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK) { + return false; + } + final Uri pictureUri = data != null && data.getData() != null + ? data.getData() : mTakePictureUri; + + // Check if the result is a content uri + if (!ContentResolver.SCHEME_CONTENT.equals(pictureUri.getScheme())) { + Log.e(TAG, "Invalid pictureUri scheme: " + pictureUri.getScheme()); + EventLog.writeEvent(0x534e4554, "172939189", -1, pictureUri.getPath()); + return false; + } + + switch (requestCode) { + case REQUEST_CODE_CROP_PHOTO: + mActivity.returnUriResult(pictureUri); + return true; + case REQUEST_CODE_TAKE_PHOTO: + case REQUEST_CODE_CHOOSE_PHOTO: + if (mTakePictureUri.equals(pictureUri)) { + if (PhotoCapabilityUtils.canCropPhoto(mActivity)) { + cropPhoto(); + } else { + onPhotoNotCropped(pictureUri); + } + } else { + copyAndCropPhoto(pictureUri); + } + return true; + } + return false; + } + + void takePhoto() { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE_SECURE); + appendOutputExtra(intent, mTakePictureUri); + mActivity.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); + } + + void choosePhoto() { + Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES, null); + intent.setType("image/*"); + mActivity.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); + } + + private void copyAndCropPhoto(final Uri pictureUri) { + // TODO: Replace AsyncTask + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + final ContentResolver cr = mActivity.getContentResolver(); + try (InputStream in = cr.openInputStream(pictureUri); + OutputStream out = cr.openOutputStream(mTakePictureUri)) { + Streams.copy(in, out); + } catch (IOException e) { + Log.w(TAG, "Failed to copy photo", e); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + if (!mActivity.isFinishing() && !mActivity.isDestroyed()) { + cropPhoto(); + } + } + }.execute(); + } + + private void cropPhoto() { + // TODO: Use a public intent, when there is one. + Intent intent = new Intent("com.android.camera.action.CROP"); + intent.setDataAndType(mTakePictureUri, "image/*"); + appendOutputExtra(intent, mCropPictureUri); + appendCropExtras(intent); + if (intent.resolveActivity(mActivity.getPackageManager()) != null) { + try { + StrictMode.disableDeathOnFileUriExposure(); + mActivity.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); + } finally { + StrictMode.enableDeathOnFileUriExposure(); + } + } else { + onPhotoNotCropped(mTakePictureUri); + } + } + + private void appendOutputExtra(Intent intent, Uri pictureUri) { + intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri)); + } + + private void appendCropExtras(Intent intent) { + intent.putExtra("crop", "true"); + intent.putExtra("scale", true); + intent.putExtra("scaleUpIfNeeded", true); + intent.putExtra("aspectX", 1); + intent.putExtra("aspectY", 1); + intent.putExtra("outputX", mPhotoSize); + intent.putExtra("outputY", mPhotoSize); + } + + private void onPhotoNotCropped(final Uri data) { + // TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change + new AsyncTask() { + @Override + protected Bitmap doInBackground(Void... params) { + // Scale and crop to a square aspect ratio + Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(croppedImage); + Bitmap fullImage; + try { + InputStream imageStream = mActivity.getContentResolver() + .openInputStream(data); + fullImage = BitmapFactory.decodeStream(imageStream); + } catch (FileNotFoundException fe) { + return null; + } + if (fullImage != null) { + int rotation = getRotation(mActivity, data); + final int squareSize = Math.min(fullImage.getWidth(), + fullImage.getHeight()); + final int left = (fullImage.getWidth() - squareSize) / 2; + final int top = (fullImage.getHeight() - squareSize) / 2; + + Matrix matrix = new Matrix(); + RectF rectSource = new RectF(left, top, + left + squareSize, top + squareSize); + RectF rectDest = new RectF(0, 0, mPhotoSize, mPhotoSize); + matrix.setRectToRect(rectSource, rectDest, Matrix.ScaleToFit.CENTER); + matrix.postRotate(rotation, mPhotoSize / 2f, mPhotoSize / 2f); + canvas.drawBitmap(fullImage, matrix, new Paint()); + return croppedImage; + } else { + // Bah! Got nothin. + return null; + } + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + saveBitmapToFile(bitmap, new File(mImagesDir, CROP_PICTURE_FILE_NAME)); + mActivity.returnUriResult(mCropPictureUri); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } + + /** + * Reads the image's exif data and determines the rotation degree needed to display the image + * in portrait mode. + */ + private int getRotation(Context context, Uri selectedImage) { + int rotation = -1; + try { + InputStream imageStream = context.getContentResolver().openInputStream(selectedImage); + ExifInterface exif = new ExifInterface(imageStream); + rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1); + } catch (IOException exception) { + Log.e(TAG, "Error while getting rotation", exception); + } + + switch (rotation) { + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: + return 0; + } + } + + private void saveBitmapToFile(Bitmap bitmap, File file) { + try { + OutputStream os = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, os); + os.flush(); + os.close(); + } catch (IOException e) { + Log.e(TAG, "Cannot create temp file", e); + } + } + + private static int getPhotoSize(Context context) { + try (Cursor cursor = context.getContentResolver().query( + ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, + new String[]{ContactsContract.DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null)) { + if (cursor != null) { + cursor.moveToFirst(); + return cursor.getInt(0); + } else { + return DEFAULT_PHOTO_SIZE; + } + } + } + + private Uri createTempImageUri(Context context, String fileName, boolean purge) { + final File fullPath = new File(mImagesDir, fileName); + if (purge) { + fullPath.delete(); + } + return FileProvider.getUriForFile(context, mFileAuthority, fullPath); + } +} 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..50015e6533999 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java @@ -0,0 +1,334 @@ +/* + * 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.ContentResolver; +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.net.Uri; +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.internal.util.UserIcons; +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; + +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"; + + private boolean mWaitingForActivityResult; + + private FooterButton mDoneButton; + private AvatarAdapter mAdapter; + + private AvatarPhotoController mAvatarPhotoController; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.trySetDynamicColor(this); + setContentView(R.layout.avatar_picker); + setUpButtons(); + + RecyclerView recyclerView = findViewById(R.id.avatar_grid); + mAdapter = new AvatarAdapter(); + recyclerView.setAdapter(mAdapter); + recyclerView.setLayoutManager(new GridLayoutManager(this, + getResources().getInteger(R.integer.avatar_picker_columns))); + + restoreState(savedInstanceState); + + mAvatarPhotoController = new AvatarPhotoController( + this, mWaitingForActivityResult, getFileAuthority()); + } + + private void setUpButtons() { + GlifLayout glifLayout = findViewById(R.id.glif_layout); + 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 -> mAdapter.returnSelectionResult()) + .build(); + mDoneButton.setEnabled(false); + + mixin.setSecondaryButton(secondaryButton); + mixin.setPrimaryButton(mDoneButton); + } + + private String getFileAuthority() { + String authority = getIntent().getStringExtra(EXTRA_FILE_AUTHORITY); + if (authority == null) { + throw new IllegalStateException("File authority must be provided"); + } + return authority; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mWaitingForActivityResult = false; + mAvatarPhotoController.onActivityResult(requestCode, resultCode, data); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult); + outState.putInt(KEY_SELECTED_POSITION, mAdapter.mSelectedPosition); + super.onSaveInstanceState(outState); + } + + private void restoreState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false); + mAdapter.mSelectedPosition = + savedInstanceState.getInt(KEY_SELECTED_POSITION, AvatarAdapter.NONE); + } + } + + @Override + public void startActivityForResult(Intent intent, int requestCode) { + mWaitingForActivityResult = true; + super.startActivityForResult(intent, requestCode); + } + + 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(); + } + + private class AvatarAdapter extends RecyclerView.Adapter { + + private static final int NONE = -1; + + private final int mTakePhotoPosition; + private final int mChoosePhotoPosition; + private final int mPreselectedImageStartPosition; + + private final List mImageDrawables; + private final TypedArray mPreselectedImages; + private final int[] mUserIconColors; + private int mSelectedPosition = NONE; + + AvatarAdapter() { + final boolean canTakePhoto = + PhotoCapabilityUtils.canTakePhoto(AvatarPickerActivity.this); + final boolean canChoosePhoto = + PhotoCapabilityUtils.canChoosePhoto(AvatarPickerActivity.this); + mTakePhotoPosition = (canTakePhoto ? 0 : NONE); + mChoosePhotoPosition = (canChoosePhoto ? (canTakePhoto ? 1 : 0) : NONE); + mPreselectedImageStartPosition = (canTakePhoto ? 1 : 0) + (canChoosePhoto ? 1 : 0); + + mPreselectedImages = getResources().obtainTypedArray(R.array.avatar_images); + mUserIconColors = UserIcons.getUserIconColors(getResources()); + mImageDrawables = buildDrawableList(); + } + + @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) { + if (position == mTakePhotoPosition) { + viewHolder.setDrawable(getDrawable(R.drawable.avatar_take_photo_circled)); + viewHolder.setClickListener(view -> mAvatarPhotoController.takePhoto()); + + } else if (position == mChoosePhotoPosition) { + viewHolder.setDrawable(getDrawable(R.drawable.avatar_choose_photo_circled)); + viewHolder.setClickListener(view -> mAvatarPhotoController.choosePhoto()); + + } else if (position >= mPreselectedImageStartPosition) { + viewHolder.setSelected(position == mSelectedPosition); + viewHolder.setDrawable(mImageDrawables.get(indexFromPosition(position))); + viewHolder.setClickListener(view -> { + if (mSelectedPosition == position) { + deselect(position); + } else { + select(position); + } + }); + } + } + + @Override + public int getItemCount() { + 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) { + Bitmap bitmap = drawable.getBitmap(); + + RoundedBitmapDrawable roundedBitmapDrawable = + RoundedBitmapDrawableFactory.create(getResources(), bitmap); + roundedBitmapDrawable.setCircular(true); + + return roundedBitmapDrawable; + } + + private int indexFromPosition(int position) { + return position - mPreselectedImageStartPosition; + } + + 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 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]); + } + } + + private Uri uriForResourceId(int resourceId) { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(getResources().getResourcePackageName(resourceId)) + .appendPath(getResources().getResourceTypeName(resourceId)) + .appendPath(getResources().getResourceEntryName(resourceId)) + .build(); + } + } + + 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..80ee86f5e4891 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,16 +214,21 @@ 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 EditUserPhotoController createEditUserPhotoController(Activity activity, ActivityStarter activityStarter, ImageView userPhotoView) { return new EditUserPhotoController(activity, activityStarter, userPhotoView, - mSavedPhoto, mWaitingForActivityResult, mFileAuthority); + mSavedPhoto, mFileAuthority); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java index f9584a3e15e9a..f8bb38b5978ec 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java @@ -16,46 +16,21 @@ package com.android.settingslib.users; +import android.annotation.NonNull; import android.app.Activity; -import android.content.ClipData; -import android.content.ContentResolver; -import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.RectF; import android.graphics.drawable.Drawable; -import android.media.ExifInterface; import android.net.Uri; -import android.os.AsyncTask; -import android.os.StrictMode; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.ContactsContract.DisplayPhoto; -import android.provider.MediaStore; -import android.util.EventLog; import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.ImageView; -import android.widget.ListPopupWindow; -import android.widget.TextView; - -import androidx.core.content.FileProvider; +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 libcore.io.Streams; +import com.android.settingslib.utils.ThreadUtils; import java.io.File; import java.io.FileNotFoundException; @@ -63,8 +38,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; +import java.util.concurrent.ExecutionException; /** * This class contains logic for starting activities to take/choose/crop photo, reads and transforms @@ -75,45 +49,30 @@ public class EditUserPhotoController { // It seems that this class generates custom request codes and they may // collide with ours, these values are very unlikely to have a conflict. - 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; - // 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; + private static final int REQUEST_CODE_PICK_AVATAR = 1004; private static final String IMAGES_DIR = "multi_user"; - private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; - private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto.jpg"; private static final String NEW_USER_PHOTO_FILE_NAME = "NewUserPhoto.png"; - private final int mPhotoSize; - private final Activity mActivity; private final ActivityStarter mActivityStarter; private final ImageView mImageView; private final String mFileAuthority; private final File mImagesDir; - private final Uri mCropPictureUri; - private final Uri mTakePictureUri; - private Bitmap mNewUserPhotoBitmap; private Drawable mNewUserPhotoDrawable; public EditUserPhotoController(Activity activity, ActivityStarter activityStarter, - ImageView view, Bitmap bitmap, boolean waiting, String fileAuthority) { + ImageView view, Bitmap bitmap, String fileAuthority) { mActivity = activity; mActivityStarter = activityStarter; - mImageView = view; mFileAuthority = fileAuthority; mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR); mImagesDir.mkdir(); - mCropPictureUri = createTempImageUri(activity, CROP_PICTURE_FILE_NAME, !waiting); - mTakePictureUri = createTempImageUri(activity, TAKE_PICTURE_FILE_NAME, !waiting); - mPhotoSize = getPhotoSize(activity); - mImageView.setOnClickListener(v -> showUpdatePhotoPopup()); + mImageView = view; + mImageView.setOnClickListener(v -> showAvatarPicker()); mNewUserPhotoBitmap = bitmap; } @@ -125,32 +84,19 @@ public class EditUserPhotoController { if (resultCode != Activity.RESULT_OK) { return false; } - final Uri pictureUri = data != null && data.getData() != null - ? data.getData() : mTakePictureUri; - // Check if the result is a content uri - if (!ContentResolver.SCHEME_CONTENT.equals(pictureUri.getScheme())) { - Log.e(TAG, "Invalid pictureUri scheme: " + pictureUri.getScheme()); - EventLog.writeEvent(0x534e4554, "172939189", -1, pictureUri.getPath()); - return false; - } + 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; + } - switch (requestCode) { - case REQUEST_CODE_CROP_PHOTO: - onPhotoCropped(pictureUri); - return true; - case REQUEST_CODE_TAKE_PHOTO: - case REQUEST_CODE_CHOOSE_PHOTO: - if (mTakePictureUri.equals(pictureUri)) { - if (PhotoCapabilityUtils.canCropPhoto(mActivity)) { - cropPhoto(); - } else { - onPhotoNotCropped(pictureUri); - } - } else { - copyAndCropPhoto(pictureUri); - } - return true; } return false; } @@ -159,224 +105,60 @@ public class EditUserPhotoController { return mNewUserPhotoDrawable; } - private void showUpdatePhotoPopup() { - final Context context = mImageView.getContext(); - final boolean canTakePhoto = PhotoCapabilityUtils.canTakePhoto(context); - final boolean canChoosePhoto = PhotoCapabilityUtils.canChoosePhoto(context); - - if (!canTakePhoto && !canChoosePhoto) { - return; - } - - final List items = new ArrayList<>(); - - if (canTakePhoto) { - final String title = context.getString(R.string.user_image_take_photo); - items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON, - this::takePhoto)); - } - - if (canChoosePhoto) { - final String title = context.getString(R.string.user_image_choose_photo); - items.add(new RestrictedMenuItem(context, title, UserManager.DISALLOW_SET_USER_ICON, - this::choosePhoto)); - } - - final ListPopupWindow listPopupWindow = new ListPopupWindow(context); - - listPopupWindow.setAnchorView(mImageView); - listPopupWindow.setModal(true); - listPopupWindow.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); - listPopupWindow.setAdapter(new RestrictedPopupMenuAdapter(context, items)); - - final int width = Math.max(mImageView.getWidth(), context.getResources() - .getDimensionPixelSize(R.dimen.update_user_photo_popup_min_width)); - listPopupWindow.setWidth(width); - listPopupWindow.setDropDownGravity(Gravity.START); - - listPopupWindow.setOnItemClickListener((parent, view, position, id) -> { - listPopupWindow.dismiss(); - final RestrictedMenuItem item = - (RestrictedMenuItem) parent.getAdapter().getItem(position); - item.doAction(); - }); - - listPopupWindow.show(); + private void showAvatarPicker() { + Intent intent = new Intent(mImageView.getContext(), AvatarPickerActivity.class); + intent.putExtra(AvatarPickerActivity.EXTRA_FILE_AUTHORITY, mFileAuthority); + mActivityStarter.startActivityForResult(intent, REQUEST_CODE_PICK_AVATAR); } - private void takePhoto() { - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE_SECURE); - appendOutputExtra(intent, mTakePictureUri); - mActivityStarter.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); - } + 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)); - private void choosePhoto() { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); - intent.setType("image/*"); - appendOutputExtra(intent, mTakePictureUri); - mActivityStarter.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); - } - - private void copyAndCropPhoto(final Uri pictureUri) { - // TODO: Replace AsyncTask - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - final ContentResolver cr = mActivity.getContentResolver(); - try (InputStream in = cr.openInputStream(pictureUri); - OutputStream out = cr.openOutputStream(mTakePictureUri)) { - Streams.copy(in, out); - } catch (IOException e) { - Log.w(TAG, "Failed to copy photo", e); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - if (!mActivity.isFinishing() && !mActivity.isDestroyed()) { - cropPhoto(); - } - } - }.execute(); - } - - private void cropPhoto() { - // TODO: Use a public intent, when there is one. - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(mTakePictureUri, "image/*"); - appendOutputExtra(intent, mCropPictureUri); - appendCropExtras(intent); - if (intent.resolveActivity(mActivity.getPackageManager()) != null) { - try { - StrictMode.disableDeathOnFileUriExposure(); - mActivityStarter.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); - } finally { - StrictMode.enableDeathOnFileUriExposure(); - } - } else { - onPhotoNotCropped(mTakePictureUri); + ThreadUtils.postOnMainThread(() -> onPhotoProcessed(bitmap)); + }).get(); + } catch (InterruptedException | ExecutionException e) { + Log.e(TAG, "Error processing default icon", e); } } - private void appendOutputExtra(Intent intent, Uri pictureUri) { - intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri); - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setClipData(ClipData.newRawUri(MediaStore.EXTRA_OUTPUT, pictureUri)); - } - - private void appendCropExtras(Intent intent) { - intent.putExtra("crop", "true"); - intent.putExtra("scale", true); - intent.putExtra("scaleUpIfNeeded", true); - intent.putExtra("aspectX", 1); - intent.putExtra("aspectY", 1); - intent.putExtra("outputX", mPhotoSize); - intent.putExtra("outputY", mPhotoSize); + 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 onPhotoNotCropped(final Uri data) { - // TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change - new AsyncTask() { - @Override - protected Bitmap doInBackground(Void... params) { - // Scale and crop to a square aspect ratio - Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, - Config.ARGB_8888); - Canvas canvas = new Canvas(croppedImage); - Bitmap fullImage; - try { - InputStream imageStream = mActivity.getContentResolver() - .openInputStream(data); - fullImage = BitmapFactory.decodeStream(imageStream); - } catch (FileNotFoundException fe) { - return null; - } - if (fullImage != null) { - int rotation = getRotation(mActivity, data); - final int squareSize = Math.min(fullImage.getWidth(), - fullImage.getHeight()); - final int left = (fullImage.getWidth() - squareSize) / 2; - final int top = (fullImage.getHeight() - squareSize) / 2; - - Matrix matrix = new Matrix(); - RectF rectSource = new RectF(left, top, - left + squareSize, top + squareSize); - RectF rectDest = new RectF(0, 0, mPhotoSize, mPhotoSize); - matrix.setRectToRect(rectSource, rectDest, Matrix.ScaleToFit.CENTER); - matrix.postRotate(rotation, mPhotoSize / 2f, mPhotoSize / 2f); - canvas.drawBitmap(fullImage, matrix, new Paint()); - return croppedImage; - } else { - // Bah! Got nothin. - return null; - } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - onPhotoProcessed(bitmap); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); - } - - /** - * Reads the image's exif data and determines the rotation degree needed to display the image - * in portrait mode. - */ - private int getRotation(Context context, Uri selectedImage) { - int rotation = -1; - try { - InputStream imageStream = context.getContentResolver().openInputStream(selectedImage); - ExifInterface exif = new ExifInterface(imageStream); - rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1); - } catch (IOException exception) { - Log.e(TAG, "Error while getting rotation", exception); - } - - switch (rotation) { - case ExifInterface.ORIENTATION_ROTATE_90: - return 90; - case ExifInterface.ORIENTATION_ROTATE_180: - return 180; - case ExifInterface.ORIENTATION_ROTATE_270: - return 270; - default: - return 0; - } + }); } private void onPhotoProcessed(Bitmap bitmap) { @@ -386,29 +168,6 @@ public class EditUserPhotoController { .getInstance(mImageView.getContext(), mNewUserPhotoBitmap); mImageView.setImageDrawable(mNewUserPhotoDrawable); } - new File(mImagesDir, TAKE_PICTURE_FILE_NAME).delete(); - new File(mImagesDir, CROP_PICTURE_FILE_NAME).delete(); - } - - private static int getPhotoSize(Context context) { - try (Cursor cursor = context.getContentResolver().query( - DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, - new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null)) { - if (cursor != null) { - cursor.moveToFirst(); - return cursor.getInt(0); - } else { - return DEFAULT_PHOTO_SIZE; - } - } - } - - private Uri createTempImageUri(Context context, String fileName, boolean purge) { - final File fullPath = new File(mImagesDir, fileName); - if (purge) { - fullPath.delete(); - } - return FileProvider.getUriForFile(context, mFileAuthority, fullPath); } File saveNewUserPhotoBitmap() { @@ -435,84 +194,4 @@ public class EditUserPhotoController { void removeNewUserPhotoBitmapFile() { new File(mImagesDir, NEW_USER_PHOTO_FILE_NAME).delete(); } - - private static final class RestrictedMenuItem { - private final Context mContext; - private final String mTitle; - private final Runnable mAction; - private final RestrictedLockUtils.EnforcedAdmin mAdmin; - // Restriction may be set by system or something else via UserManager.setUserRestriction(). - private final boolean mIsRestrictedByBase; - - /** - * The menu item, used for popup menu. Any element of such a menu can be disabled by admin. - * - * @param context A context. - * @param title The title of the menu item. - * @param restriction The restriction, that if is set, blocks the menu item. - * @param action The action on menu item click. - */ - RestrictedMenuItem(Context context, String title, String restriction, - Runnable action) { - mContext = context; - mTitle = title; - mAction = action; - - final int myUserId = UserHandle.myUserId(); - mAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, - restriction, myUserId); - mIsRestrictedByBase = RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, - restriction, myUserId); - } - - @Override - public String toString() { - return mTitle; - } - - void doAction() { - if (isRestrictedByBase()) { - return; - } - - if (isRestrictedByAdmin()) { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mAdmin); - return; - } - - mAction.run(); - } - - boolean isRestrictedByAdmin() { - return mAdmin != null; - } - - boolean isRestrictedByBase() { - return mIsRestrictedByBase; - } - } - - /** - * Provide this adapter to ListPopupWindow.setAdapter() to have a popup window menu, where - * any element can be restricted by admin (profile owner or device owner). - */ - private static final class RestrictedPopupMenuAdapter extends ArrayAdapter { - RestrictedPopupMenuAdapter(Context context, List items) { - super(context, R.layout.restricted_popup_menu_item, R.id.text, items); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final View view = super.getView(position, convertView, parent); - final RestrictedMenuItem item = getItem(position); - final TextView text = (TextView) view.findViewById(R.id.text); - final ImageView image = (ImageView) view.findViewById(R.id.restricted_icon); - - text.setEnabled(!item.isRestrictedByAdmin() && !item.isRestrictedByBase()); - image.setVisibility(item.isRestrictedByAdmin() && !item.isRestrictedByBase() - ? ImageView.VISIBLE : ImageView.GONE); - - return view; - } - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/PhotoCapabilityUtils.java b/packages/SettingsLib/src/com/android/settingslib/users/PhotoCapabilityUtils.java index 165c2808f16d9..b8615a7e5aa92 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/PhotoCapabilityUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/PhotoCapabilityUtils.java @@ -40,12 +40,12 @@ public class PhotoCapabilityUtils { /** * Check if the current user can perform any activity for - * android.intent.action.GET_CONTENT action for images. + * ACTION_PICK_IMAGES action for images. * Returns false if the device is currently locked and * requires a PIN, pattern or password to unlock. */ public static boolean canChoosePhoto(Context context) { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); intent.setType("image/*"); boolean canPerformActivityForGetImage = context.getPackageManager().queryIntentActivities(intent, 0).size() > 0; 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);