diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java index 61b8911acea45..0cb2c0b22a4c5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java @@ -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() { - @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() { - @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); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java index 1e1dfae9f7ac3..75bb70a123c37 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java @@ -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() { diff --git a/packages/SettingsLib/tests/integ/AndroidManifest.xml b/packages/SettingsLib/tests/integ/AndroidManifest.xml index da808dd541416..2a4dfdd84c63b 100644 --- a/packages/SettingsLib/tests/integ/AndroidManifest.xml +++ b/packages/SettingsLib/tests/integ/AndroidManifest.xml @@ -25,10 +25,19 @@ - + + + + + + + + + diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AvatarPhotoControllerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AvatarPhotoControllerTest.java new file mode 100644 index 0000000000000..9ebdba3002660 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AvatarPhotoControllerTest.java @@ -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 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 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(); + } + +}