Skip to content

Commit 2ec3f47

Browse files
BeMacizedfotiDim
authored andcommitted
[image_picker] Change storage location for camera captures to internal cache on Android, to comply with new Google Play storage requirements. (flutter#3956)
1 parent fade1cc commit 2ec3f47

File tree

10 files changed

+58
-102
lines changed

10 files changed

+58
-102
lines changed

packages/image_picker/image_picker/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 0.8.0
2+
3+
* BREAKING CHANGE: Changed storage location for captured images and videos to internal cache on Android,
4+
to comply with new Google Play storage requirements. This means developers are responsible for moving
5+
the image or video to a different location in case more permanent storage is required. Other applications
6+
will no longer be able to access images or videos captured unless they are moved to a publicly accessible location.
7+
* Updated Mockito to fix Android tests.
8+
19
## 0.7.5+4
210
* Migrate maven repo from jcenter to mavenCentral.
311

packages/image_picker/image_picker/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ Add the following keys to your _Info.plist_ file, located in `<project root>/ios
1919

2020
### Android
2121

22-
#### API < 29
2322
No configuration required - the plugin should work out of the box.
2423

25-
#### API 29+
24+
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.
2625

27-
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.
26+
**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.
27+
If you require your picked image to be stored permanently, it is your responsibility to move it to a more permanent location.
2828

2929
### Example
3030

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2-
package="io.flutter.plugins.imagepicker">
3-
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
4-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
2+
package="io.flutter.plugins.imagepicker">
53

64
<application>
75
<provider
@@ -11,7 +9,7 @@
119
android:grantUriPermissions="true">
1210
<meta-data
1311
android:name="android.support.FILE_PROVIDER_PATHS"
14-
android:resource="@xml/flutter_image_picker_file_paths"/>
12+
android:resource="@xml/flutter_image_picker_file_paths" />
1513
</provider>
1614
</application>
17-
</manifest>
15+
</manifest>

packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,8 @@ enum CameraDevice {
4242
* means that the chooseImageFromGallery() or takeImageWithCamera() method was called at least
4343
* twice. In this case, stop executing and finish with an error.
4444
*
45-
* <p>2. Check that a required runtime permission has been granted. The chooseImageFromGallery()
46-
* method checks if the {@link Manifest.permission#READ_EXTERNAL_STORAGE} permission has been
47-
* granted. Similarly, the takeImageWithCamera() method checks that {@link
48-
* Manifest.permission#CAMERA} has been granted.
45+
* <p>2. Check that a required runtime permission has been granted. The takeImageWithCamera() method
46+
* checks that {@link Manifest.permission#CAMERA} has been granted.
4947
*
5048
* <p>The permission check can end up in two different outcomes:
5149
*
@@ -76,17 +74,15 @@ public class ImagePickerDelegate
7674
PluginRegistry.RequestPermissionsResultListener {
7775
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY = 2342;
7876
@VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343;
79-
@VisibleForTesting static final int REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION = 2344;
8077
@VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345;
8178
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352;
8279
@VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353;
83-
@VisibleForTesting static final int REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION = 2354;
8480
@VisibleForTesting static final int REQUEST_CAMERA_VIDEO_PERMISSION = 2355;
8581

8682
@VisibleForTesting final String fileProviderName;
8783

8884
private final Activity activity;
89-
private final File externalFilesDirectory;
85+
@VisibleForTesting final File externalFilesDirectory;
9086
private final ImageResizer imageResizer;
9187
private final ImagePickerCache cache;
9288
private final PermissionManager permissionManager;
@@ -257,12 +253,6 @@ public void chooseVideoFromGallery(MethodCall methodCall, MethodChannel.Result r
257253
return;
258254
}
259255

260-
if (!permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) {
261-
permissionManager.askForPermission(
262-
Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION);
263-
return;
264-
}
265-
266256
launchPickVideoFromGalleryIntent();
267257
}
268258

@@ -322,12 +312,6 @@ public void chooseImageFromGallery(MethodCall methodCall, MethodChannel.Result r
322312
return;
323313
}
324314

325-
if (!permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) {
326-
permissionManager.askForPermission(
327-
Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION);
328-
return;
329-
}
330-
331315
launchPickImageFromGalleryIntent();
332316
}
333317

@@ -424,16 +408,6 @@ public boolean onRequestPermissionsResult(
424408
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
425409

426410
switch (requestCode) {
427-
case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION:
428-
if (permissionGranted) {
429-
launchPickImageFromGalleryIntent();
430-
}
431-
break;
432-
case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION:
433-
if (permissionGranted) {
434-
launchPickVideoFromGalleryIntent();
435-
}
436-
break;
437411
case REQUEST_CAMERA_IMAGE_PERMISSION:
438412
if (permissionGranted) {
439413
launchTakeImageWithCameraIntent();
@@ -450,10 +424,6 @@ public boolean onRequestPermissionsResult(
450424

451425
if (!permissionGranted) {
452426
switch (requestCode) {
453-
case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION:
454-
case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION:
455-
finishWithError("photo_access_denied", "The user did not allow photo access.");
456-
break;
457427
case REQUEST_CAMERA_IMAGE_PERMISSION:
458428
case REQUEST_CAMERA_VIDEO_PERMISSION:
459429
finishWithError("camera_access_denied", "The user did not allow camera access.");

packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import android.app.Activity;
88
import android.app.Application;
99
import android.os.Bundle;
10-
import android.os.Environment;
1110
import android.os.Handler;
1211
import android.os.Looper;
1312
import androidx.annotation.NonNull;
@@ -216,11 +215,11 @@ private void tearDown() {
216215
application = null;
217216
}
218217

219-
private final ImagePickerDelegate constructDelegate(final Activity setupActivity) {
218+
@VisibleForTesting
219+
final ImagePickerDelegate constructDelegate(final Activity setupActivity) {
220220
final ImagePickerCache cache = new ImagePickerCache(setupActivity);
221221

222-
final File externalFilesDirectory =
223-
setupActivity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
222+
final File externalFilesDirectory = setupActivity.getCacheDir();
224223
final ExifDataCopier exifDataCopier = new ExifDataCopier();
225224
final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier);
226225
return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache);
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<paths>
3-
<external-path name="external_files" path="."/>
4-
</paths>
3+
<cache-path name="cached_files" path="."/>
4+
</paths>

packages/image_picker/image_picker/example/android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ flutter {
6060

6161
dependencies {
6262
testImplementation 'junit:junit:4.12'
63-
testImplementation 'org.mockito:mockito-core:2.17.0'
63+
testImplementation 'org.mockito:mockito-core:3.10.0'
6464
androidTestImplementation 'androidx.test:runner:1.1.1'
6565
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
6666
testImplementation 'androidx.test:core:1.2.0'

packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java

Lines changed: 16 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.mockito.ArgumentMatchers.any;
1010
import static org.mockito.ArgumentMatchers.eq;
1111
import static org.mockito.Mockito.mock;
12+
import static org.mockito.Mockito.times;
1213
import static org.mockito.Mockito.verify;
1314
import static org.mockito.Mockito.verifyNoMoreInteractions;
1415
import static org.mockito.Mockito.when;
@@ -25,6 +26,8 @@
2526
import org.junit.Before;
2627
import org.junit.Test;
2728
import org.mockito.Mock;
29+
import org.mockito.MockedStatic;
30+
import org.mockito.Mockito;
2831
import org.mockito.MockitoAnnotations;
2932

3033
public class ImagePickerDelegateTest {
@@ -100,20 +103,6 @@ public void chooseImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyAc
100103
verifyNoMoreInteractions(mockResult);
101104
}
102105

103-
@Test
104-
public void chooseImageFromGallery_WhenHasNoExternalStoragePermission_RequestsForPermission() {
105-
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
106-
.thenReturn(false);
107-
108-
ImagePickerDelegate delegate = createDelegate();
109-
delegate.chooseImageFromGallery(mockMethodCall, mockResult);
110-
111-
verify(mockPermissionManager)
112-
.askForPermission(
113-
Manifest.permission.READ_EXTERNAL_STORAGE,
114-
ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION);
115-
}
116-
117106
@Test
118107
public void
119108
chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
@@ -193,47 +182,21 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis
193182
}
194183

195184
@Test
196-
public void
197-
onRequestPermissionsResult_WhenReadExternalStoragePermissionDenied_FinishesWithError() {
198-
ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
199-
200-
delegate.onRequestPermissionsResult(
201-
ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION,
202-
new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
203-
new int[] {PackageManager.PERMISSION_DENIED});
204-
205-
verify(mockResult).error("photo_access_denied", "The user did not allow photo access.", null);
206-
verifyNoMoreInteractions(mockResult);
207-
}
208-
209-
@Test
210-
public void
211-
onRequestChooseImagePermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseImageFromGalleryIntent() {
212-
ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
185+
public void takeImageWithCamera_WritesImageToCacheDirectory() {
186+
when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true);
187+
when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
213188

214-
delegate.onRequestPermissionsResult(
215-
ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION,
216-
new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
217-
new int[] {PackageManager.PERMISSION_GRANTED});
189+
MockedStatic<File> mockStaticFile = Mockito.mockStatic(File.class);
190+
mockStaticFile
191+
.when(() -> File.createTempFile(any(), any(), any()))
192+
.thenReturn(new File("/tmpfile"));
218193

219-
verify(mockActivity)
220-
.startActivityForResult(
221-
any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY));
222-
}
223-
224-
@Test
225-
public void
226-
onRequestChooseVideoPermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseVideoFromGalleryIntent() {
227-
ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
228-
229-
delegate.onRequestPermissionsResult(
230-
ImagePickerDelegate.REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION,
231-
new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
232-
new int[] {PackageManager.PERMISSION_GRANTED});
194+
ImagePickerDelegate delegate = createDelegate();
195+
delegate.takeImageWithCamera(mockMethodCall, mockResult);
233196

234-
verify(mockActivity)
235-
.startActivityForResult(
236-
any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY));
197+
mockStaticFile.verify(
198+
() -> File.createTempFile(any(), eq(".jpg"), eq(new File("/image_picker_cache"))),
199+
times(1));
237200
}
238201

239202
@Test
@@ -394,7 +357,7 @@ public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_Finishes
394357
private ImagePickerDelegate createDelegate() {
395358
return new ImagePickerDelegate(
396359
mockActivity,
397-
null,
360+
new File("/image_picker_cache"),
398361
mockImageResizer,
399362
null,
400363
null,

packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
package io.flutter.plugins.imagepicker;
66

7+
import static org.hamcrest.core.IsEqual.equalTo;
8+
import static org.junit.Assert.assertThat;
79
import static org.junit.Assert.assertTrue;
810
import static org.mockito.ArgumentMatchers.any;
911
import static org.mockito.ArgumentMatchers.eq;
12+
import static org.mockito.Mockito.times;
1013
import static org.mockito.Mockito.verify;
1114
import static org.mockito.Mockito.verifyZeroInteractions;
1215
import static org.mockito.Mockito.when;
@@ -15,6 +18,7 @@
1518
import android.app.Application;
1619
import io.flutter.plugin.common.MethodCall;
1720
import io.flutter.plugin.common.MethodChannel;
21+
import java.io.File;
1822
import java.util.HashMap;
1923
import java.util.Map;
2024
import org.junit.Before;
@@ -149,6 +153,20 @@ public void onConstructor_WhenContextTypeIsActivity_ShouldNotCrash() {
149153
"No exception thrown when ImagePickerPlugin() ran with context instanceof Activity", true);
150154
}
151155

156+
@Test
157+
public void constructDelegate_ShouldUseInternalCacheDirectory() {
158+
File mockDirectory = new File("/mockpath");
159+
when(mockActivity.getCacheDir()).thenReturn(mockDirectory);
160+
161+
ImagePickerDelegate delegate = plugin.constructDelegate(mockActivity);
162+
163+
verify(mockActivity, times(1)).getCacheDir();
164+
assertThat(
165+
"Delegate uses cache directory for storing camera captures",
166+
delegate.externalFilesDirectory,
167+
equalTo(mockDirectory));
168+
}
169+
152170
private MethodCall buildMethodCall(String method, final int source) {
153171
final Map<String, Object> arguments = new HashMap<>();
154172
arguments.put("source", source);

packages/image_picker/image_picker/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image
33
library, and taking new pictures with the camera.
44
repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
6-
version: 0.7.5+4
6+
version: 0.8.0
77

88
environment:
99
sdk: ">=2.12.0 <3.0.0"

0 commit comments

Comments
 (0)