[image_picker] Change storage location for camera captures to internal cache on Android, to comply with new Google Play storage requirements. (#3956)
diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md
index e412cf5..515845e 100644
--- a/packages/image_picker/image_picker/CHANGELOG.md
+++ b/packages/image_picker/image_picker/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.8.0
+
+* BREAKING CHANGE: Changed storage location for captured images and videos to internal cache on Android,
+to comply with new Google Play storage requirements. This means developers are responsible for moving
+the image or video to a different location in case more permanent storage is required. Other applications
+will no longer be able to access images or videos captured unless they are moved to a publicly accessible location.
+* Updated Mockito to fix Android tests.
+
## 0.7.5+4
* Migrate maven repo from jcenter to mavenCentral.
diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md
index ca8ad76..1de12bc 100755
--- a/packages/image_picker/image_picker/README.md
+++ b/packages/image_picker/image_picker/README.md
@@ -19,12 +19,12 @@
### Android
-#### API < 29
No configuration required - the plugin should work out of the box.
-#### API 29+
+It is no longer required to add `android:requestLegacyExternalStorage="true"` as an attribute to the `<application>` tag in AndroidManifest.xml, as `image_picker` has been updated to make use of scoped storage.
-Add `android:requestLegacyExternalStorage="true"` as an attribute to the `<application>` tag in AndroidManifest.xml. The [attribute](https://developer.android.com/training/data-storage/compatibility) is `false` by default on apps targeting Android Q.
+**Note:** Images and videos picked using the camera are saved to your application's local cache, and should therefore be expected to only be around temporarily.
+If you require your picked image to be stored permanently, it is your responsibility to move it to a more permanent location.
### Example
diff --git a/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml b/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml
index f0bc86f..5d1773e 100755
--- a/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml
+++ b/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml
@@ -1,7 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="io.flutter.plugins.imagepicker">
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ package="io.flutter.plugins.imagepicker">
<application>
<provider
@@ -11,7 +9,7 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
- android:resource="@xml/flutter_image_picker_file_paths"/>
+ android:resource="@xml/flutter_image_picker_file_paths" />
</provider>
</application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
index 29d7c85..c934b54 100644
--- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
+++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
@@ -42,10 +42,8 @@
* means that the chooseImageFromGallery() or takeImageWithCamera() method was called at least
* twice. In this case, stop executing and finish with an error.
*
- * <p>2. Check that a required runtime permission has been granted. The chooseImageFromGallery()
- * method checks if the {@link Manifest.permission#READ_EXTERNAL_STORAGE} permission has been
- * granted. Similarly, the takeImageWithCamera() method checks that {@link
- * Manifest.permission#CAMERA} has been granted.
+ * <p>2. Check that a required runtime permission has been granted. The takeImageWithCamera() method
+ * checks that {@link Manifest.permission#CAMERA} has been granted.
*
* <p>The permission check can end up in two different outcomes:
*
@@ -76,17 +74,15 @@
PluginRegistry.RequestPermissionsResultListener {
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY = 2342;
@VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343;
- @VisibleForTesting static final int REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION = 2344;
@VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345;
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352;
@VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353;
- @VisibleForTesting static final int REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION = 2354;
@VisibleForTesting static final int REQUEST_CAMERA_VIDEO_PERMISSION = 2355;
@VisibleForTesting final String fileProviderName;
private final Activity activity;
- private final File externalFilesDirectory;
+ @VisibleForTesting final File externalFilesDirectory;
private final ImageResizer imageResizer;
private final ImagePickerCache cache;
private final PermissionManager permissionManager;
@@ -257,12 +253,6 @@
return;
}
- if (!permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) {
- permissionManager.askForPermission(
- Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION);
- return;
- }
-
launchPickVideoFromGalleryIntent();
}
@@ -322,12 +312,6 @@
return;
}
- if (!permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) {
- permissionManager.askForPermission(
- Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION);
- return;
- }
-
launchPickImageFromGalleryIntent();
}
@@ -424,16 +408,6 @@
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
switch (requestCode) {
- case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION:
- if (permissionGranted) {
- launchPickImageFromGalleryIntent();
- }
- break;
- case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION:
- if (permissionGranted) {
- launchPickVideoFromGalleryIntent();
- }
- break;
case REQUEST_CAMERA_IMAGE_PERMISSION:
if (permissionGranted) {
launchTakeImageWithCameraIntent();
@@ -450,10 +424,6 @@
if (!permissionGranted) {
switch (requestCode) {
- case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION:
- case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION:
- finishWithError("photo_access_denied", "The user did not allow photo access.");
- break;
case REQUEST_CAMERA_IMAGE_PERMISSION:
case REQUEST_CAMERA_VIDEO_PERMISSION:
finishWithError("camera_access_denied", "The user did not allow camera access.");
diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
index 98b6410..bffc903 100644
--- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
+++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
@@ -7,7 +7,6 @@
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
-import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
@@ -216,11 +215,11 @@
application = null;
}
- private final ImagePickerDelegate constructDelegate(final Activity setupActivity) {
+ @VisibleForTesting
+ final ImagePickerDelegate constructDelegate(final Activity setupActivity) {
final ImagePickerCache cache = new ImagePickerCache(setupActivity);
- final File externalFilesDirectory =
- setupActivity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+ final File externalFilesDirectory = setupActivity.getCacheDir();
final ExifDataCopier exifDataCopier = new ExifDataCopier();
final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier);
return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache);
diff --git a/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml b/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml
index 4495c28..354418b 100644
--- a/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml
+++ b/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
- <external-path name="external_files" path="."/>
-</paths>
\ No newline at end of file
+ <cache-path name="cached_files" path="."/>
+</paths>
diff --git a/packages/image_picker/image_picker/example/android/app/build.gradle b/packages/image_picker/image_picker/example/android/app/build.gradle
index 7b25d07..cc77d33 100755
--- a/packages/image_picker/image_picker/example/android/app/build.gradle
+++ b/packages/image_picker/image_picker/example/android/app/build.gradle
@@ -60,7 +60,7 @@
dependencies {
testImplementation 'junit:junit:4.12'
- testImplementation 'org.mockito:mockito-core:2.17.0'
+ testImplementation 'org.mockito:mockito-core:3.10.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
testImplementation 'androidx.test:core:1.2.0'
diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
index a6858f2..da53b10 100644
--- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
+++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java
@@ -9,6 +9,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -25,6 +26,8 @@
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
public class ImagePickerDelegateTest {
@@ -101,20 +104,6 @@
}
@Test
- public void chooseImageFromGallery_WhenHasNoExternalStoragePermission_RequestsForPermission() {
- when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
- .thenReturn(false);
-
- ImagePickerDelegate delegate = createDelegate();
- delegate.chooseImageFromGallery(mockMethodCall, mockResult);
-
- verify(mockPermissionManager)
- .askForPermission(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION);
- }
-
- @Test
public void
chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
@@ -193,47 +182,21 @@
}
@Test
- public void
- onRequestPermissionsResult_WhenReadExternalStoragePermissionDenied_FinishesWithError() {
- ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+ public void takeImageWithCamera_WritesImageToCacheDirectory() {
+ when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true);
+ when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
- delegate.onRequestPermissionsResult(
- ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION,
- new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
- new int[] {PackageManager.PERMISSION_DENIED});
+ MockedStatic<File> mockStaticFile = Mockito.mockStatic(File.class);
+ mockStaticFile
+ .when(() -> File.createTempFile(any(), any(), any()))
+ .thenReturn(new File("/tmpfile"));
- verify(mockResult).error("photo_access_denied", "The user did not allow photo access.", null);
- verifyNoMoreInteractions(mockResult);
- }
+ ImagePickerDelegate delegate = createDelegate();
+ delegate.takeImageWithCamera(mockMethodCall, mockResult);
- @Test
- public void
- onRequestChooseImagePermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseImageFromGalleryIntent() {
- ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
-
- delegate.onRequestPermissionsResult(
- ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION,
- new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
- new int[] {PackageManager.PERMISSION_GRANTED});
-
- verify(mockActivity)
- .startActivityForResult(
- any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY));
- }
-
- @Test
- public void
- onRequestChooseVideoPermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseVideoFromGalleryIntent() {
- ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
-
- delegate.onRequestPermissionsResult(
- ImagePickerDelegate.REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION,
- new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
- new int[] {PackageManager.PERMISSION_GRANTED});
-
- verify(mockActivity)
- .startActivityForResult(
- any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY));
+ mockStaticFile.verify(
+ () -> File.createTempFile(any(), eq(".jpg"), eq(new File("/image_picker_cache"))),
+ times(1));
}
@Test
@@ -394,7 +357,7 @@
private ImagePickerDelegate createDelegate() {
return new ImagePickerDelegate(
mockActivity,
- null,
+ new File("/image_picker_cache"),
mockImageResizer,
null,
null,
diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
index 2e50a22..a0ce87f 100644
--- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
+++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
@@ -4,9 +4,12 @@
package io.flutter.plugins.imagepicker;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
@@ -15,6 +18,7 @@
import android.app.Application;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
+import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
@@ -149,6 +153,20 @@
"No exception thrown when ImagePickerPlugin() ran with context instanceof Activity", true);
}
+ @Test
+ public void constructDelegate_ShouldUseInternalCacheDirectory() {
+ File mockDirectory = new File("/mockpath");
+ when(mockActivity.getCacheDir()).thenReturn(mockDirectory);
+
+ ImagePickerDelegate delegate = plugin.constructDelegate(mockActivity);
+
+ verify(mockActivity, times(1)).getCacheDir();
+ assertThat(
+ "Delegate uses cache directory for storing camera captures",
+ delegate.externalFilesDirectory,
+ equalTo(mockDirectory));
+ }
+
private MethodCall buildMethodCall(String method, final int source) {
final Map<String, Object> arguments = new HashMap<>();
arguments.put("source", source);
diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml
index 584125f..95ea808 100755
--- a/packages/image_picker/image_picker/pubspec.yaml
+++ b/packages/image_picker/image_picker/pubspec.yaml
@@ -3,7 +3,7 @@
library, and taking new pictures with the camera.
repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 0.7.5+4
+version: 0.8.0
environment:
sdk: ">=2.12.0 <3.0.0"