Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cf4f22e8 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Revert "Prevent exfiltration of system files via avatar picker."" into tm-dev am: 95e397a8

parents d7e5f37d 95e397a8
Loading
Loading
Loading
Loading
+24 −37
Original line number Original line Diff line number Diff line
@@ -21,8 +21,6 @@ import android.content.ClipData;
import android.content.ContentResolver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Canvas;
@@ -59,9 +57,9 @@ class AvatarPhotoController {


        void startActivityForResult(Intent intent, int resultCode);
        void startActivityForResult(Intent intent, int resultCode);


        boolean startSystemActivityForResult(Intent intent, int resultCode);

        int getPhotoSize();
        int getPhotoSize();

        boolean canCropPhoto();
    }
    }


    interface ContextInjector {
    interface ContextInjector {
@@ -84,7 +82,6 @@ class AvatarPhotoController {
    private static final long DELAY_BEFORE_CROP_MILLIS = 150;
    private static final long DELAY_BEFORE_CROP_MILLIS = 150;


    private static final String IMAGES_DIR = "multi_user";
    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 CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg";
    private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto.jpg";
    private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto.jpg";


@@ -94,7 +91,6 @@ class AvatarPhotoController {
    private final ContextInjector mContextInjector;
    private final ContextInjector mContextInjector;


    private final File mImagesDir;
    private final File mImagesDir;
    private final Uri mPreCropPictureUri;
    private final Uri mCropPictureUri;
    private final Uri mCropPictureUri;
    private final Uri mTakePictureUri;
    private final Uri mTakePictureUri;


@@ -104,8 +100,6 @@ class AvatarPhotoController {


        mImagesDir = new File(mContextInjector.getCacheDir(), IMAGES_DIR);
        mImagesDir = new File(mContextInjector.getCacheDir(), IMAGES_DIR);
        mImagesDir.mkdir();
        mImagesDir.mkdir();
        mPreCropPictureUri = mContextInjector
                .createTempImageUri(mImagesDir, PRE_CROP_PICTURE_FILE_NAME, !waiting);
        mCropPictureUri =
        mCropPictureUri =
                mContextInjector.createTempImageUri(mImagesDir, CROP_PICTURE_FILE_NAME, !waiting);
                mContextInjector.createTempImageUri(mImagesDir, CROP_PICTURE_FILE_NAME, !waiting);
        mTakePictureUri =
        mTakePictureUri =
@@ -137,7 +131,7 @@ class AvatarPhotoController {
                return true;
                return true;
            case REQUEST_CODE_TAKE_PHOTO:
            case REQUEST_CODE_TAKE_PHOTO:
                if (mTakePictureUri.equals(pictureUri)) {
                if (mTakePictureUri.equals(pictureUri)) {
                    cropPhoto(pictureUri);
                    cropPhoto();
                } else {
                } else {
                    copyAndCropPhoto(pictureUri, false);
                    copyAndCropPhoto(pictureUri, false);
                }
                }
@@ -166,7 +160,7 @@ class AvatarPhotoController {
            ThreadUtils.postOnBackgroundThread(() -> {
            ThreadUtils.postOnBackgroundThread(() -> {
                final ContentResolver cr = mContextInjector.getContentResolver();
                final ContentResolver cr = mContextInjector.getContentResolver();
                try (InputStream in = cr.openInputStream(pictureUri);
                try (InputStream in = cr.openInputStream(pictureUri);
                        OutputStream out = cr.openOutputStream(mPreCropPictureUri)) {
                     OutputStream out = cr.openOutputStream(mTakePictureUri)) {
                    Streams.copy(in, out);
                    Streams.copy(in, out);
                } catch (IOException e) {
                } catch (IOException e) {
                    Log.w(TAG, "Failed to copy photo", e);
                    Log.w(TAG, "Failed to copy photo", e);
@@ -174,7 +168,7 @@ class AvatarPhotoController {
                }
                }
                Runnable cropRunnable = () -> {
                Runnable cropRunnable = () -> {
                    if (!mAvatarUi.isFinishing()) {
                    if (!mAvatarUi.isFinishing()) {
                        cropPhoto(mPreCropPictureUri);
                        cropPhoto();
                    }
                    }
                };
                };
                if (delayBeforeCrop) {
                if (delayBeforeCrop) {
@@ -189,21 +183,22 @@ class AvatarPhotoController {
        }
        }
    }
    }


    private void cropPhoto(final Uri pictureUri) {
    private void cropPhoto() {
        if (mAvatarUi.canCropPhoto()) {
            // TODO: Use a public intent, when there is one.
            // TODO: Use a public intent, when there is one.
            Intent intent = new Intent("com.android.camera.action.CROP");
            Intent intent = new Intent("com.android.camera.action.CROP");
        intent.setDataAndType(pictureUri, "image/*");
            intent.setDataAndType(mTakePictureUri, "image/*");
            appendOutputExtra(intent, mCropPictureUri);
            appendOutputExtra(intent, mCropPictureUri);
            appendCropExtras(intent);
            appendCropExtras(intent);
            try {
            try {
                StrictMode.disableDeathOnFileUriExposure();
                StrictMode.disableDeathOnFileUriExposure();
            if (mAvatarUi.startSystemActivityForResult(intent, REQUEST_CODE_CROP_PHOTO)) {
                mAvatarUi.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO);
                return;
            }
            } finally {
            } finally {
                StrictMode.enableDeathOnFileUriExposure();
                StrictMode.enableDeathOnFileUriExposure();
            }
            }
        onPhotoNotCropped(pictureUri);
        } else {
            onPhotoNotCropped(mTakePictureUri);
        }
    }
    }


    private void appendOutputExtra(Intent intent, Uri pictureUri) {
    private void appendOutputExtra(Intent intent, Uri pictureUri) {
@@ -324,24 +319,16 @@ class AvatarPhotoController {
            mActivity.startActivityForResult(intent, 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
        @Override
        public int getPhotoSize() {
        public int getPhotoSize() {
            return mActivity.getResources()
            return mActivity.getResources()
                    .getDimensionPixelSize(com.android.internal.R.dimen.user_icon_size);
                    .getDimensionPixelSize(com.android.internal.R.dimen.user_icon_size);
        }
        }

        @Override
        public boolean canCropPhoto() {
            return PhotoCapabilityUtils.canCropPhoto(mActivity);
        }
    }
    }


    static class ContextInjectorImpl implements ContextInjector {
    static class ContextInjectorImpl implements ContextInjector {
+36 −59
Original line number Original line Diff line number Diff line
@@ -34,7 +34,6 @@ import android.app.Activity;
import android.content.Context;
import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.net.Uri;
import android.provider.MediaStore;
import android.provider.MediaStore;


@@ -52,7 +51,6 @@ import org.mockito.MockitoAnnotations;
import java.io.File;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStream;


@RunWith(AndroidJUnit4.class)
@RunWith(AndroidJUnit4.class)
@@ -75,7 +73,6 @@ public class AvatarPhotoControllerTest {
    public void setUp() {
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        MockitoAnnotations.initMocks(this);
        when(mMockAvatarUi.getPhotoSize()).thenReturn(PHOTO_SIZE);
        when(mMockAvatarUi.getPhotoSize()).thenReturn(PHOTO_SIZE);
        when(mMockAvatarUi.startSystemActivityForResult(any(), anyInt())).thenReturn(true);


        mImagesDir = new File(
        mImagesDir = new File(
                InstrumentationRegistry.getTargetContext().getCacheDir(), "multi_user");
                InstrumentationRegistry.getTargetContext().getCacheDir(), "multi_user");
@@ -113,7 +110,9 @@ public class AvatarPhotoControllerTest {
    }
    }


    @Test
    @Test
    public void takePhotoIsFollowedByCrop() throws IOException {
    public void takePhotoIsFollowedByCropWhenSupported() throws IOException {
        when(mMockAvatarUi.canCropPhoto()).thenReturn(true);

        new File(mImagesDir, "file.txt").createNewFile();
        new File(mImagesDir, "file.txt").createNewFile();


        Intent intent = new Intent();
        Intent intent = new Intent();
@@ -122,12 +121,14 @@ public class AvatarPhotoControllerTest {
        mController.onActivityResult(
        mController.onActivityResult(
                REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);
                REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);


        verifyStartSystemActivityForResult(
        verifyStartActivityForResult(
                "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
                "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
    }
    }


    @Test
    @Test
    public void takePhotoIsNotFollowedByCropWhenResultCodeNotOk() throws IOException {
    public void takePhotoIsNotFollowedByCropWhenResultCodeNotOk() throws IOException {
        when(mMockAvatarUi.canCropPhoto()).thenReturn(true);

        new File(mImagesDir, "file.txt").createNewFile();
        new File(mImagesDir, "file.txt").createNewFile();


        Intent intent = new Intent();
        Intent intent = new Intent();
@@ -137,11 +138,12 @@ public class AvatarPhotoControllerTest {
                REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_CANCELED, intent);
                REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_CANCELED, intent);


        verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt());
        verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt());
        verify(mMockAvatarUi, never()).startSystemActivityForResult(any(), anyInt());
    }
    }


    @Test
    @Test
    public void takePhotoIsFollowedByCropWhenTakePhotoUriReturned() throws IOException {
    public void takePhotoIsFollowedByCropWhenTakePhotoUriReturned() throws IOException {
        when(mMockAvatarUi.canCropPhoto()).thenReturn(true);

        new File(mImagesDir, "TakeEditUserPhoto.jpg").createNewFile();
        new File(mImagesDir, "TakeEditUserPhoto.jpg").createNewFile();


        Intent intent = new Intent();
        Intent intent = new Intent();
@@ -149,12 +151,14 @@ public class AvatarPhotoControllerTest {
        mController.onActivityResult(
        mController.onActivityResult(
                REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);
                REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent);


        verifyStartSystemActivityForResult(
        verifyStartActivityForResult(
                "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
                "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
    }
    }


    @Test
    @Test
    public void choosePhotoIsFollowedByCrop() throws IOException {
    public void choosePhotoIsFollowedByCrop() throws IOException {
        when(mMockAvatarUi.canCropPhoto()).thenReturn(true);

        new File(mImagesDir, "file.txt").createNewFile();
        new File(mImagesDir, "file.txt").createNewFile();


        Intent intent = new Intent();
        Intent intent = new Intent();
@@ -163,12 +167,14 @@ public class AvatarPhotoControllerTest {
        mController.onActivityResult(
        mController.onActivityResult(
                REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_OK, intent);
                REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_OK, intent);


        verifyStartSystemActivityForResult(
        verifyStartActivityForResult(
                "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
                "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
    }
    }


    @Test
    @Test
    public void choosePhotoIsNotFollowedByCropWhenResultCodeNotOk() throws IOException {
    public void choosePhotoIsNotFollowedByCropWhenResultCodeNotOk() throws IOException {
        when(mMockAvatarUi.canCropPhoto()).thenReturn(true);

        new File(mImagesDir, "file.txt").createNewFile();
        new File(mImagesDir, "file.txt").createNewFile();


        Intent intent = new Intent();
        Intent intent = new Intent();
@@ -178,11 +184,12 @@ public class AvatarPhotoControllerTest {
                REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_CANCELED, intent);
                REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_CANCELED, intent);


        verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt());
        verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt());
        verify(mMockAvatarUi, never()).startSystemActivityForResult(any(), anyInt());
    }
    }


    @Test
    @Test
    public void choosePhotoIsFollowedByCropWhenTakePhotoUriReturned() throws IOException {
    public void choosePhotoIsFollowedByCropWhenTakePhotoUriReturned() throws IOException {
        when(mMockAvatarUi.canCropPhoto()).thenReturn(true);

        new File(mImagesDir, "TakeEditUserPhoto.jpg").createNewFile();
        new File(mImagesDir, "TakeEditUserPhoto.jpg").createNewFile();


        Intent intent = new Intent();
        Intent intent = new Intent();
@@ -190,10 +197,27 @@ public class AvatarPhotoControllerTest {
        mController.onActivityResult(
        mController.onActivityResult(
                REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_OK, intent);
                REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_OK, intent);


        verifyStartSystemActivityForResult(
        verifyStartActivityForResult(
                "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
                "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO);
    }
    }


    @Test
    public void choosePhotoIsNotFollowedByCropIntentWhenCropNotSupported() throws IOException {
        when(mMockAvatarUi.canCropPhoto()).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_CHOOSE_PHOTO, Activity.RESULT_OK, intent);

        verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt());
        verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS)).returnUriResult(mCropPhotoUri);
    }

    @Test
    @Test
    public void cropPhotoResultIsReturnedIfResultOkAndContent() {
    public void cropPhotoResultIsReturnedIfResultOkAndContent() {
        Intent intent = new Intent();
        Intent intent = new Intent();
@@ -218,58 +242,11 @@ public class AvatarPhotoControllerTest {
        verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS).times(0)).returnUriResult(mCropPhotoUri);
        verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS).times(0)).returnUriResult(mCropPhotoUri);
    }
    }


    @Test
    private void verifyStartActivityForResult(String action, int resultCode) {
    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);

        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, 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);
        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
        verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS))
        verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS))
                .startActivityForResult(captor.capture(), eq(resultCode));
                .startActivityForResult(captor.capture(), eq(resultCode));
        Intent intent = captor.getValue();
        assertThat(captor.getValue().getAction()).isEqualTo(action);
        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 {
    private void saveBitmapToFile(File file) throws IOException {