Support taking or choosing a photo in avatar picker.
This adds options to take a photo or to choose a photo to the new avatar picker. The functionality to support both of these operations, plus cropping, is largely the same as before but has been moved from EditUserPhotoController to a new AvatarPhotoController. The code from EditUserPhotoController has been moved without changes where possible. Future CLs may introduce improvements such as replacing AsyncTask. The 'choose photo' has been changed from the previous behaviour, however - it now shows the new photo picker. Bug: 215134398 Test: manual, check new UI from settings and sysui and confirm working Change-Id: I5d98902f3e8c186ad30d288b8eed544977ba0e48
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?android:attr/colorPrimary"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:left="@dimen/avatar_picker_icon_inset"
|
||||
android:right="@dimen/avatar_picker_icon_inset"
|
||||
android:top="@dimen/avatar_picker_icon_inset"
|
||||
android:bottom="@dimen/avatar_picker_icon_inset"
|
||||
android:drawable="@drawable/ic_avatar_choose_photo"/>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?android:attr/colorPrimary"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:left="@dimen/avatar_picker_icon_inset"
|
||||
android:right="@dimen/avatar_picker_icon_inset"
|
||||
android:top="@dimen/avatar_picker_icon_inset"
|
||||
android:bottom="@dimen/avatar_picker_icon_inset"
|
||||
android:drawable="@drawable/ic_avatar_take_photo"/>
|
||||
</layer-list>
|
||||
24
packages/SettingsLib/res/drawable/ic_avatar_choose_photo.xml
Normal file
24
packages/SettingsLib/res/drawable/ic_avatar_choose_photo.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/colorPrimary"
|
||||
android:pathData="M9,14h10l-3.45,-4.5 -2.3,3 -1.55,-2zM8,18q-0.825,0 -1.412,-0.587Q6,16.825 6,16L6,4q0,-0.825 0.588,-1.413Q7.175,2 8,2h12q0.825,0 1.413,0.587Q22,3.175 22,4v12q0,0.825 -0.587,1.413Q20.825,18 20,18zM8,16h12L20,4L8,4v12zM4,22q-0.825,0 -1.413,-0.587Q2,20.825 2,20L2,6h2v14h14v2zM8,4v12L8,4z"/>
|
||||
</vector>
|
||||
24
packages/SettingsLib/res/drawable/ic_avatar_take_photo.xml
Normal file
24
packages/SettingsLib/res/drawable/ic_avatar_take_photo.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/colorPrimary"
|
||||
android:pathData="M12,17.5q1.875,0 3.188,-1.313Q16.5,14.876 16.5,13q0,-1.875 -1.313,-3.188Q13.876,8.5 12,8.5q-1.875,0 -3.188,1.313Q7.5,11.124 7.5,13q0,1.875 1.313,3.188Q10.124,17.5 12,17.5zM4,21q-0.825,0 -1.413,-0.587Q2,19.825 2,19L2,7q0,-0.825 0.587,-1.412Q3.175,5 4,5h3.15L9,3h6l1.85,2L20,5q0.825,0 1.413,0.588Q22,6.175 22,7v12q0,0.825 -0.587,1.413Q20.825,21 20,21zM20,19L20,7L4,7v12zM4,19L4,7v12z"/>
|
||||
</vector>
|
||||
@@ -107,4 +107,6 @@
|
||||
<dimen name="avatar_picker_padding">6dp</dimen>
|
||||
<dimen name="avatar_picker_margin">2dp</dimen>
|
||||
|
||||
<dimen name="avatar_picker_icon_inset">25dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -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<Void, Void, Void>() {
|
||||
@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<Void, Void, Bitmap>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<AvatarViewHolder> {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EditUserPhotoController.RestrictedMenuItem> 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<Void, Void, Void>() {
|
||||
@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<Void, Void, Bitmap>() {
|
||||
@@ -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<Void, Void, Bitmap>() {
|
||||
@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<RestrictedMenuItem> {
|
||||
RestrictedPopupMenuAdapter(Context context, List<RestrictedMenuItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user