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_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_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/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml index e46d69e135d68..120df76218b37 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -107,4 +107,6 @@ 6dp 2dp + 25dp + 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..8dfd1bd5d05c6 --- /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.returnResult(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.returnResult(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 index 4cd107df1d090..2d9809d2f69ed 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java @@ -17,11 +17,13 @@ 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; @@ -46,19 +48,38 @@ import com.google.android.setupdesign.util.ThemeHelper; */ public class AvatarPickerActivity extends Activity { - static final String EXTRA_AVATAR_INDEX = "avatar_index"; + static final String EXTRA_FILE_AUTHORITY = "file_authority"; + 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(); - GlifLayout glifLayout = findViewById(R.id.glif_layout); 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 = @@ -70,23 +91,53 @@ public class AvatarPickerActivity extends Activity { mDoneButton = new FooterButton.Builder(this) .setText("Done") - .setListener(view -> confirmSelection()) + .setListener(view -> returnResult(mAdapter.uriForSelection())) .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); + 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 returnResult(Uri uri) { + Intent resultData = new Intent(); + resultData.setData(uri); + setResult(RESULT_OK, resultData); finish(); } @@ -98,12 +149,23 @@ public class AvatarPickerActivity extends Activity { private class AvatarAdapter extends RecyclerView.Adapter { private static final int NONE = -1; - private static final int AVATAR_START_POSITION = 0; + + private final int mTakePhotoPosition; + private final int mChoosePhotoPosition; + private final int mPreselectedImageStartPosition; private final TypedArray mImageDrawables; 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); + mImageDrawables = getResources().obtainTypedArray(R.array.avatar_images); } @@ -117,26 +179,36 @@ public class AvatarPickerActivity extends Activity { @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); + 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) { + Drawable drawable = mImageDrawables.getDrawable(indexFromPosition(position)); + if (drawable instanceof BitmapDrawable) { + drawable = circularDrawableFrom((BitmapDrawable) drawable); } else { - select(position); + 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(); + return mPreselectedImageStartPosition + mImageDrawables.length(); } private Drawable circularDrawableFrom(BitmapDrawable drawable) { @@ -150,7 +222,7 @@ public class AvatarPickerActivity extends Activity { } private int indexFromPosition(int position) { - return position - AVATAR_START_POSITION; + return position - mPreselectedImageStartPosition; } private void select(int position) { @@ -169,6 +241,24 @@ public class AvatarPickerActivity extends Activity { notifyItemChanged(position); mDoneButton.setEnabled(false); } + + private Uri uriForSelection() { + int resourceId = + mImageDrawables.getResourceId(indexFromPosition(mSelectedPosition), -1); + if (resourceId == -1) { + throw new IllegalStateException("Preselected avatar images must be resources."); + } + return uriForResourceId(resourceId); + } + + 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 { diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java index fb0346dacb53a..80ee86f5e4891 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java @@ -229,6 +229,6 @@ public class EditUserInfoController { 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 ee4c9fde9b6e5..03b6d2444f225 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java @@ -17,57 +17,23 @@ 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.content.res.Resources; -import android.content.res.TypedArray; -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.BitmapDrawable; 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.settingslib.R; -import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.drawable.CircleFramedDrawable; -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; -import java.util.ArrayList; -import java.util.List; /** * This class contains logic for starting activities to take/choose/crop photo, reads and transforms @@ -78,45 +44,29 @@ 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; 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; 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 = view; mImageView.setOnClickListener(v -> showAvatarPicker()); mNewUserPhotoBitmap = bitmap; } @@ -129,44 +79,12 @@ 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; - } - - 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; - 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()); - } - } + if (requestCode == REQUEST_CODE_PICK_AVATAR) { + if (data.getData() != null) { + onPhotoCropped(data.getData()); + } + return true; } return false; } @@ -177,126 +95,10 @@ public class EditUserPhotoController { 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 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 takePhoto() { - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE_SECURE); - appendOutputExtra(intent, mTakePictureUri); - mActivityStarter.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); - } - - 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); - } - } - - 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 onPhotoCropped(final Uri data) { // TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change new AsyncTask() { @@ -329,77 +131,6 @@ public class EditUserPhotoController { }.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) { if (bitmap != null) { mNewUserPhotoBitmap = bitmap; @@ -407,29 +138,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() { @@ -456,84 +164,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;