Merge changes Idf1ab608,I70f3e8dc into tm-dev
* changes: Prevent exfiltration of system files via avatar picker. Add tests for AvatarPhotoController.
This commit is contained in:
@@ -21,7 +21,8 @@ import android.content.ClipData;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
@@ -30,15 +31,15 @@ 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 com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import libcore.io.Streams;
|
||||
|
||||
import java.io.File;
|
||||
@@ -47,39 +48,64 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
class AvatarPhotoController {
|
||||
|
||||
interface AvatarUi {
|
||||
boolean isFinishing();
|
||||
|
||||
void returnUriResult(Uri uri);
|
||||
|
||||
void startActivityForResult(Intent intent, int resultCode);
|
||||
|
||||
boolean startSystemActivityForResult(Intent intent, int resultCode);
|
||||
|
||||
int getPhotoSize();
|
||||
}
|
||||
|
||||
interface ContextInjector {
|
||||
File getCacheDir();
|
||||
|
||||
Uri createTempImageUri(File parentDir, String fileName, boolean purge);
|
||||
|
||||
ContentResolver getContentResolver();
|
||||
}
|
||||
|
||||
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;
|
||||
static final int REQUEST_CODE_CHOOSE_PHOTO = 1001;
|
||||
static final int REQUEST_CODE_TAKE_PHOTO = 1002;
|
||||
static final int REQUEST_CODE_CROP_PHOTO = 1003;
|
||||
|
||||
private static final String IMAGES_DIR = "multi_user";
|
||||
private static final String PRE_CROP_PICTURE_FILE_NAME = "PreCropEditUserPhoto.jpg";
|
||||
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 AvatarUi mAvatarUi;
|
||||
private final ContextInjector mContextInjector;
|
||||
|
||||
private final File mImagesDir;
|
||||
private final Uri mPreCropPictureUri;
|
||||
private final Uri mCropPictureUri;
|
||||
private final Uri mTakePictureUri;
|
||||
|
||||
AvatarPhotoController(AvatarPickerActivity activity, boolean waiting, String fileAuthority) {
|
||||
mActivity = activity;
|
||||
mFileAuthority = fileAuthority;
|
||||
AvatarPhotoController(AvatarUi avatarUi, ContextInjector contextInjector, boolean waiting) {
|
||||
mAvatarUi = avatarUi;
|
||||
mContextInjector = contextInjector;
|
||||
|
||||
mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR);
|
||||
mImagesDir = new File(mContextInjector.getCacheDir(), IMAGES_DIR);
|
||||
mImagesDir.mkdir();
|
||||
mCropPictureUri = createTempImageUri(activity, CROP_PICTURE_FILE_NAME, !waiting);
|
||||
mTakePictureUri = createTempImageUri(activity, TAKE_PICTURE_FILE_NAME, !waiting);
|
||||
mPhotoSize = getPhotoSize(activity);
|
||||
mPreCropPictureUri = mContextInjector
|
||||
.createTempImageUri(mImagesDir, PRE_CROP_PICTURE_FILE_NAME, !waiting);
|
||||
mCropPictureUri =
|
||||
mContextInjector.createTempImageUri(mImagesDir, CROP_PICTURE_FILE_NAME, !waiting);
|
||||
mTakePictureUri =
|
||||
mContextInjector.createTempImageUri(mImagesDir, TAKE_PICTURE_FILE_NAME, !waiting);
|
||||
mPhotoSize = mAvatarUi.getPhotoSize();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,16 +128,12 @@ class AvatarPhotoController {
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_CROP_PHOTO:
|
||||
mActivity.returnUriResult(pictureUri);
|
||||
mAvatarUi.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);
|
||||
}
|
||||
cropPhoto(pictureUri);
|
||||
} else {
|
||||
copyAndCropPhoto(pictureUri);
|
||||
}
|
||||
@@ -123,55 +145,52 @@ class AvatarPhotoController {
|
||||
void takePhoto() {
|
||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE_SECURE);
|
||||
appendOutputExtra(intent, mTakePictureUri);
|
||||
mActivity.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
|
||||
mAvatarUi.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);
|
||||
mAvatarUi.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 {
|
||||
ThreadUtils.postOnBackgroundThread(() -> {
|
||||
final ContentResolver cr = mContextInjector.getContentResolver();
|
||||
try (InputStream in = cr.openInputStream(pictureUri);
|
||||
OutputStream out = cr.openOutputStream(mTakePictureUri)) {
|
||||
OutputStream out = cr.openOutputStream(mPreCropPictureUri)) {
|
||||
Streams.copy(in, out);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to copy photo", e);
|
||||
return;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
if (!mActivity.isFinishing() && !mActivity.isDestroyed()) {
|
||||
cropPhoto();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
ThreadUtils.postOnMainThread(() -> {
|
||||
if (!mAvatarUi.isFinishing()) {
|
||||
cropPhoto(mPreCropPictureUri);
|
||||
}
|
||||
});
|
||||
}).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.e(TAG, "Error performing copy-and-crop", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void cropPhoto() {
|
||||
private void cropPhoto(final Uri pictureUri) {
|
||||
// TODO: Use a public intent, when there is one.
|
||||
Intent intent = new Intent("com.android.camera.action.CROP");
|
||||
intent.setDataAndType(mTakePictureUri, "image/*");
|
||||
intent.setDataAndType(pictureUri, "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();
|
||||
try {
|
||||
StrictMode.disableDeathOnFileUriExposure();
|
||||
if (mAvatarUi.startSystemActivityForResult(intent, REQUEST_CODE_CROP_PHOTO)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
onPhotoNotCropped(mTakePictureUri);
|
||||
} finally {
|
||||
StrictMode.enableDeathOnFileUriExposure();
|
||||
}
|
||||
onPhotoNotCropped(pictureUri);
|
||||
}
|
||||
|
||||
private void appendOutputExtra(Intent intent, Uri pictureUri) {
|
||||
@@ -192,24 +211,22 @@ class AvatarPhotoController {
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
ThreadUtils.postOnBackgroundThread(() -> {
|
||||
// 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()
|
||||
InputStream imageStream = mContextInjector.getContentResolver()
|
||||
.openInputStream(data);
|
||||
fullImage = BitmapFactory.decodeStream(imageStream);
|
||||
} catch (FileNotFoundException fe) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
if (fullImage != null) {
|
||||
int rotation = getRotation(mActivity, data);
|
||||
int rotation = getRotation(data);
|
||||
final int squareSize = Math.min(fullImage.getWidth(),
|
||||
fullImage.getHeight());
|
||||
final int left = (fullImage.getWidth() - squareSize) / 2;
|
||||
@@ -222,29 +239,27 @@ class AvatarPhotoController {
|
||||
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;
|
||||
}
|
||||
}
|
||||
saveBitmapToFile(croppedImage, new File(mImagesDir, CROP_PICTURE_FILE_NAME));
|
||||
|
||||
@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);
|
||||
ThreadUtils.postOnMainThread(() -> {
|
||||
mAvatarUi.returnUriResult(mCropPictureUri);
|
||||
});
|
||||
}
|
||||
}).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.e(TAG, "Error performing internal crop", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
private int getRotation(Uri selectedImage) {
|
||||
int rotation = -1;
|
||||
try {
|
||||
InputStream imageStream = context.getContentResolver().openInputStream(selectedImage);
|
||||
InputStream imageStream =
|
||||
mContextInjector.getContentResolver().openInputStream(selectedImage);
|
||||
ExifInterface exif = new ExifInterface(imageStream);
|
||||
rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
|
||||
} catch (IOException exception) {
|
||||
@@ -274,24 +289,74 @@ class AvatarPhotoController {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
static class AvatarUiImpl implements AvatarUi {
|
||||
private final AvatarPickerActivity mActivity;
|
||||
|
||||
AvatarUiImpl(AvatarPickerActivity activity) {
|
||||
mActivity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinishing() {
|
||||
return mActivity.isFinishing() || mActivity.isDestroyed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void returnUriResult(Uri uri) {
|
||||
mActivity.returnUriResult(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivityForResult(Intent intent, int resultCode) {
|
||||
mActivity.startActivityForResult(intent, resultCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startSystemActivityForResult(Intent intent, int code) {
|
||||
ActivityInfo info = intent.resolveActivityInfo(mActivity.getPackageManager(),
|
||||
PackageManager.MATCH_SYSTEM_ONLY);
|
||||
if (info == null) {
|
||||
Log.w(TAG, "No system package activity could be found for code " + code);
|
||||
return false;
|
||||
}
|
||||
intent.setPackage(info.packageName);
|
||||
mActivity.startActivityForResult(intent, code);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPhotoSize() {
|
||||
return mActivity.getResources()
|
||||
.getDimensionPixelSize(com.android.internal.R.dimen.user_icon_size);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri createTempImageUri(Context context, String fileName, boolean purge) {
|
||||
final File fullPath = new File(mImagesDir, fileName);
|
||||
if (purge) {
|
||||
fullPath.delete();
|
||||
static class ContextInjectorImpl implements ContextInjector {
|
||||
private final Context mContext;
|
||||
private final String mFileAuthority;
|
||||
|
||||
ContextInjectorImpl(Context context, String fileAuthority) {
|
||||
mContext = context;
|
||||
mFileAuthority = fileAuthority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getCacheDir() {
|
||||
return mContext.getCacheDir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri createTempImageUri(File parentDir, String fileName, boolean purge) {
|
||||
final File fullPath = new File(parentDir, fileName);
|
||||
if (purge) {
|
||||
fullPath.delete();
|
||||
}
|
||||
return FileProvider.getUriForFile(mContext, mFileAuthority, fullPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentResolver getContentResolver() {
|
||||
return mContext.getContentResolver();
|
||||
}
|
||||
return FileProvider.getUriForFile(context, mFileAuthority, fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,9 @@ public class AvatarPickerActivity extends Activity {
|
||||
restoreState(savedInstanceState);
|
||||
|
||||
mAvatarPhotoController = new AvatarPhotoController(
|
||||
this, mWaitingForActivityResult, getFileAuthority());
|
||||
new AvatarPhotoController.AvatarUiImpl(this),
|
||||
new AvatarPhotoController.ContextInjectorImpl(this, getFileAuthority()),
|
||||
mWaitingForActivityResult);
|
||||
}
|
||||
|
||||
private void setUpButtons() {
|
||||
|
||||
@@ -25,10 +25,19 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||
|
||||
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
<activity android:name=".drawer.SettingsDrawerActivityTest$TestActivity"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="com.android.settingslib.test"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
20
packages/SettingsLib/tests/integ/res/xml/file_paths.xml
Normal file
20
packages/SettingsLib/tests/integ/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Offer access to files under Context.getCacheDir() -->
|
||||
<cache-path name="my_cache" />
|
||||
</paths>
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* 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 static com.android.settingslib.users.AvatarPhotoController.REQUEST_CODE_CHOOSE_PHOTO;
|
||||
import static com.android.settingslib.users.AvatarPhotoController.REQUEST_CODE_CROP_PHOTO;
|
||||
import static com.android.settingslib.users.AvatarPhotoController.REQUEST_CODE_TAKE_PHOTO;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.anyInt;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AvatarPhotoControllerTest {
|
||||
|
||||
private static final long TIMEOUT_MILLIS = 5000;
|
||||
private static final int PHOTO_SIZE = 200;
|
||||
|
||||
@Mock AvatarPhotoController.AvatarUi mMockAvatarUi;
|
||||
|
||||
private File mImagesDir;
|
||||
private AvatarPhotoController mController;
|
||||
private Uri mTakePhotoUri = Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/TakeEditUserPhoto.jpg");
|
||||
private Uri mCropPhotoUri = Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/CropEditUserPhoto.jpg");
|
||||
private Context mContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
when(mMockAvatarUi.getPhotoSize()).thenReturn(PHOTO_SIZE);
|
||||
when(mMockAvatarUi.startSystemActivityForResult(any(), anyInt())).thenReturn(true);
|
||||
|
||||
mImagesDir = new File(
|
||||
InstrumentationRegistry.getTargetContext().getCacheDir(), "multi_user");
|
||||
mImagesDir.mkdir();
|
||||
|
||||
AvatarPhotoController.ContextInjector contextInjector =
|
||||
new AvatarPhotoController.ContextInjectorImpl(
|
||||
InstrumentationRegistry.getTargetContext(), "com.android.settingslib.test");
|
||||
mController = new AvatarPhotoController(mMockAvatarUi, contextInjector, false);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
mImagesDir.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void takePhotoHasCorrectIntentAndResultCode() {
|
||||
mController.takePhoto();
|
||||
|
||||
verifyStartActivityForResult(
|
||||
MediaStore.ACTION_IMAGE_CAPTURE_SECURE, REQUEST_CODE_TAKE_PHOTO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void choosePhotoHasCorrectIntentAndResultCode() {
|
||||
mController.choosePhoto();
|
||||
|
||||
verifyStartActivityForResult(
|
||||
MediaStore.ACTION_PICK_IMAGES, REQUEST_CODE_CHOOSE_PHOTO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void takePhotoIsFollowedByCrop() throws IOException {
|
||||
new File(mImagesDir, "file.txt").createNewFile();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/file.txt"));
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);
|
||||
|
||||
verifyStartSystemActivityForResult(
|
||||
"com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void takePhotoIsNotFollowedByCropWhenResultCodeNotOk() throws IOException {
|
||||
new File(mImagesDir, "file.txt").createNewFile();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/file.txt"));
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_CANCELED, intent);
|
||||
|
||||
verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt());
|
||||
verify(mMockAvatarUi, never()).startSystemActivityForResult(any(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void takePhotoIsFollowedByCropWhenTakePhotoUriReturned() throws IOException {
|
||||
new File(mImagesDir, "TakeEditUserPhoto.jpg").createNewFile();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(mTakePhotoUri);
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);
|
||||
|
||||
verifyStartSystemActivityForResult(
|
||||
"com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void takePhotoIsNotFollowedByCropIntentWhenCropNotSupported() throws IOException {
|
||||
when(mMockAvatarUi.startSystemActivityForResult(any(), anyInt())).thenReturn(false);
|
||||
|
||||
File file = new File(mImagesDir, "file.txt");
|
||||
saveBitmapToFile(file);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/file.txt"));
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);
|
||||
|
||||
verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt());
|
||||
verify(mMockAvatarUi, never()).startSystemActivityForResult(any(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void choosePhotoIsFollowedByCrop() throws IOException {
|
||||
new File(mImagesDir, "file.txt").createNewFile();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/file.txt"));
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_OK, intent);
|
||||
|
||||
verifyStartSystemActivityForResult(
|
||||
"com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void choosePhotoIsNotFollowedByCropWhenResultCodeNotOk() throws IOException {
|
||||
new File(mImagesDir, "file.txt").createNewFile();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/file.txt"));
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_CANCELED, intent);
|
||||
|
||||
verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt());
|
||||
verify(mMockAvatarUi, never()).startSystemActivityForResult(any(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void choosePhotoIsFollowedByCropWhenTakePhotoUriReturned() throws IOException {
|
||||
new File(mImagesDir, "TakeEditUserPhoto.jpg").createNewFile();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(mTakePhotoUri);
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_OK, intent);
|
||||
|
||||
verifyStartSystemActivityForResult(
|
||||
"com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cropPhotoResultIsReturnedIfResultOkAndContent() {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(mCropPhotoUri);
|
||||
mController.onActivityResult(REQUEST_CODE_CROP_PHOTO, Activity.RESULT_OK, intent);
|
||||
verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS)).returnUriResult(mCropPhotoUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cropPhotoResultIsNotReturnedIfResultCancel() {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(mCropPhotoUri);
|
||||
mController.onActivityResult(REQUEST_CODE_CROP_PHOTO, Activity.RESULT_CANCELED, intent);
|
||||
verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS).times(0)).returnUriResult(mCropPhotoUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cropPhotoResultIsNotReturnedIfResultNotContent() {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse("file://test"));
|
||||
mController.onActivityResult(REQUEST_CODE_CROP_PHOTO, Activity.RESULT_OK, intent);
|
||||
verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS).times(0)).returnUriResult(mCropPhotoUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cropDoesNotUseTakePhotoUri() throws IOException {
|
||||
new File(mImagesDir, "file.txt").createNewFile();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/file.txt"));
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);
|
||||
|
||||
Intent startIntent = verifyStartSystemActivityForResult(
|
||||
"com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
|
||||
assertThat(startIntent.getData()).isNotEqualTo(mTakePhotoUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void internalCropUsedIfNoSystemCropperFound() throws IOException {
|
||||
when(mMockAvatarUi.startSystemActivityForResult(any(), anyInt())).thenReturn(false);
|
||||
|
||||
new File(mImagesDir, "file.txt").createNewFile();
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(
|
||||
"content://com.android.settingslib.test/my_cache/multi_user/file.txt"));
|
||||
mController.onActivityResult(
|
||||
REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);
|
||||
|
||||
Intent startIntent = verifyStartSystemActivityForResult(
|
||||
"com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
|
||||
assertThat(startIntent.getData()).isNotEqualTo(mTakePhotoUri);
|
||||
|
||||
verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS)).returnUriResult(mCropPhotoUri);
|
||||
|
||||
InputStream imageStream = mContext.getContentResolver().openInputStream(mCropPhotoUri);
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(imageStream);
|
||||
assertThat(bitmap.getWidth()).isEqualTo(PHOTO_SIZE);
|
||||
assertThat(bitmap.getHeight()).isEqualTo(PHOTO_SIZE);
|
||||
}
|
||||
|
||||
private Intent verifyStartActivityForResult(String action, int resultCode) {
|
||||
ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
|
||||
verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS))
|
||||
.startActivityForResult(captor.capture(), eq(resultCode));
|
||||
Intent intent = captor.getValue();
|
||||
assertThat(intent.getAction()).isEqualTo(action);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Intent verifyStartSystemActivityForResult(String action, int resultCode) {
|
||||
ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
|
||||
verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS))
|
||||
.startSystemActivityForResult(captor.capture(), eq(resultCode));
|
||||
Intent intent = captor.getValue();
|
||||
assertThat(intent.getAction()).isEqualTo(action);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private void saveBitmapToFile(File file) throws IOException {
|
||||
Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
|
||||
OutputStream os = new FileOutputStream(file);
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
|
||||
os.flush();
|
||||
os.close();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user