[image_picker] Reverted removal of camera permission request (#4021)
diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md
index e83ab72..b913bbf 100644
--- a/packages/image_picker/image_picker/CHANGELOG.md
+++ b/packages/image_picker/image_picker/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.0+3
+
+* Readded request for camera permissions.
+
## 0.8.0+2
* Fix a rotation problem where when camera is chosen as a source and additional parameters are added.
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 41df851..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
@@ -4,6 +4,7 @@
package io.flutter.plugins.imagepicker;
+import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -14,6 +15,7 @@
import android.os.Build;
import android.provider.MediaStore;
import androidx.annotation.VisibleForTesting;
+import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
@@ -40,7 +42,19 @@
* means that the chooseImageFromGallery() or takeImageWithCamera() method was called at least
* twice. In this case, stop executing and finish with an error.
*
- * <p>2. Launch the gallery or camera for picking the image, depending on whether
+ * <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:
+ *
+ * <p>A) If the permission has already been granted, continue with picking the image from gallery or
+ * camera.
+ *
+ * <p>B) If the permission hasn't already been granted, ask for the permission from the user. If the
+ * user grants the permission, proceed with step #3. If the user denies the permission, stop doing
+ * anything else and finish with a null result.
+ *
+ * <p>3. Launch the gallery or camera for picking the image, depending on whether
* chooseImageFromGallery() or takeImageWithCamera() was called.
*
* <p>This can end up in three different outcomes:
@@ -55,11 +69,15 @@
*
* <p>C) User cancels picking an image. Finish with null result.
*/
-public class ImagePickerDelegate implements PluginRegistry.ActivityResultListener {
+public class ImagePickerDelegate
+ implements PluginRegistry.ActivityResultListener,
+ 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_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_CAMERA_VIDEO_PERMISSION = 2355;
@VisibleForTesting final String fileProviderName;
@@ -67,11 +85,20 @@
@VisibleForTesting final File externalFilesDirectory;
private final ImageResizer imageResizer;
private final ImagePickerCache cache;
+ private final PermissionManager permissionManager;
private final IntentResolver intentResolver;
private final FileUriResolver fileUriResolver;
private final FileUtils fileUtils;
private CameraDevice cameraDevice;
+ interface PermissionManager {
+ boolean isPermissionGranted(String permissionName);
+
+ void askForPermission(String permissionName, int requestCode);
+
+ boolean needRequestCameraPermission();
+ }
+
interface IntentResolver {
boolean resolveActivity(Intent intent);
}
@@ -102,6 +129,23 @@
null,
null,
cache,
+ new PermissionManager() {
+ @Override
+ public boolean isPermissionGranted(String permissionName) {
+ return ActivityCompat.checkSelfPermission(activity, permissionName)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ @Override
+ public void askForPermission(String permissionName, int requestCode) {
+ ActivityCompat.requestPermissions(activity, new String[] {permissionName}, requestCode);
+ }
+
+ @Override
+ public boolean needRequestCameraPermission() {
+ return ImagePickerUtils.needRequestCameraPermission(activity);
+ }
+ },
new IntentResolver() {
@Override
public boolean resolveActivity(Intent intent) {
@@ -143,6 +187,7 @@
final MethodChannel.Result result,
final MethodCall methodCall,
final ImagePickerCache cache,
+ final PermissionManager permissionManager,
final IntentResolver intentResolver,
final FileUriResolver fileUriResolver,
final FileUtils fileUtils) {
@@ -152,6 +197,7 @@
this.fileProviderName = activity.getPackageName() + ".flutter.image_provider";
this.pendingResult = result;
this.methodCall = methodCall;
+ this.permissionManager = permissionManager;
this.intentResolver = intentResolver;
this.fileUriResolver = fileUriResolver;
this.fileUtils = fileUtils;
@@ -223,6 +269,13 @@
return;
}
+ if (needRequestCameraPermission()
+ && !permissionManager.isPermissionGranted(Manifest.permission.CAMERA)) {
+ permissionManager.askForPermission(
+ Manifest.permission.CAMERA, REQUEST_CAMERA_VIDEO_PERMISSION);
+ return;
+ }
+
launchTakeVideoWithCameraIntent();
}
@@ -275,9 +328,22 @@
return;
}
+ if (needRequestCameraPermission()
+ && !permissionManager.isPermissionGranted(Manifest.permission.CAMERA)) {
+ permissionManager.askForPermission(
+ Manifest.permission.CAMERA, REQUEST_CAMERA_IMAGE_PERMISSION);
+ return;
+ }
launchTakeImageWithCameraIntent();
}
+ private boolean needRequestCameraPermission() {
+ if (permissionManager == null) {
+ return false;
+ }
+ return permissionManager.needRequestCameraPermission();
+ }
+
private void launchTakeImageWithCameraIntent() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (cameraDevice == CameraDevice.FRONT) {
@@ -336,6 +402,39 @@
}
@Override
+ public boolean onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ boolean permissionGranted =
+ grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+
+ switch (requestCode) {
+ case REQUEST_CAMERA_IMAGE_PERMISSION:
+ if (permissionGranted) {
+ launchTakeImageWithCameraIntent();
+ }
+ break;
+ case REQUEST_CAMERA_VIDEO_PERMISSION:
+ if (permissionGranted) {
+ launchTakeVideoWithCameraIntent();
+ }
+ break;
+ default:
+ return false;
+ }
+
+ if (!permissionGranted) {
+ switch (requestCode) {
+ case REQUEST_CAMERA_IMAGE_PERMISSION:
+ case REQUEST_CAMERA_VIDEO_PERMISSION:
+ finishWithError("camera_access_denied", "The user did not allow camera access.");
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY:
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 b4e7e8a..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
@@ -192,9 +192,11 @@
// V1 embedding setup for activity listeners.
application.registerActivityLifecycleCallbacks(observer);
registrar.addActivityResultListener(delegate);
+ registrar.addRequestPermissionsResultListener(delegate);
} else {
// V2 embedding setup for activity listeners.
activityBinding.addActivityResultListener(delegate);
+ activityBinding.addRequestPermissionsResultListener(delegate);
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
lifecycle.addObserver(observer);
}
@@ -202,6 +204,7 @@
private void tearDown() {
activityBinding.removeActivityResultListener(delegate);
+ activityBinding.removeRequestPermissionsResultListener(delegate);
activityBinding = null;
lifecycle.removeObserver(observer);
lifecycle = null;
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 5b66814..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
@@ -14,6 +14,7 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -39,6 +40,7 @@
@Mock ImageResizer mockImageResizer;
@Mock MethodCall mockMethodCall;
@Mock MethodChannel.Result mockResult;
+ @Mock ImagePickerDelegate.PermissionManager mockPermissionManager;
@Mock ImagePickerDelegate.IntentResolver mockIntentResolver;
@Mock FileUtils mockFileUtils;
@Mock Intent mockIntent;
@@ -102,7 +104,11 @@
}
@Test
- public void chooseImageFromGallery_LaunchesChooseFromGalleryIntent() {
+ public void
+ chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
+ when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
+ .thenReturn(true);
+
ImagePickerDelegate delegate = createDelegate();
delegate.chooseImageFromGallery(mockMethodCall, mockResult);
@@ -122,30 +128,49 @@
}
@Test
- public void
- takeImageWithCamera_WhenAnActivityCanHandleCameraIntent_LaunchesTakeWithCameraIntent() {
+ public void takeImageWithCamera_WhenHasNoCameraPermission_RequestsForPermission() {
+ when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(false);
+ when(mockPermissionManager.needRequestCameraPermission()).thenReturn(true);
+
+ ImagePickerDelegate delegate = createDelegate();
+ delegate.takeImageWithCamera(mockMethodCall, mockResult);
+
+ verify(mockPermissionManager)
+ .askForPermission(
+ Manifest.permission.CAMERA, ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION);
+ }
+
+ @Test
+ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermission() {
+ when(mockPermissionManager.needRequestCameraPermission()).thenReturn(false);
when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
- MockedStatic<File> mockStaticFile = Mockito.mockStatic(File.class);
- mockStaticFile
- .when(() -> File.createTempFile(any(), any(), any()))
- .thenReturn(new File("/tmpfile"));
+ ImagePickerDelegate delegate = createDelegate();
+ delegate.takeImageWithCamera(mockMethodCall, mockResult);
- try {
- ImagePickerDelegate delegate = createDelegate();
- delegate.takeImageWithCamera(mockMethodCall, mockResult);
-
- verify(mockActivity)
- .startActivityForResult(
- any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA));
- } finally {
- mockStaticFile.close();
- }
+ verify(mockActivity)
+ .startActivityForResult(
+ any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA));
}
@Test
public void
- takeImageWithCamera_WhenNoActivityToHandleCameraIntent_FinishesWithNoCamerasAvailableError() {
+ takeImageWithCamera_WhenHasCameraPermission_AndAnActivityCanHandleCameraIntent_LaunchesTakeWithCameraIntent() {
+ when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true);
+ when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
+
+ ImagePickerDelegate delegate = createDelegate();
+ delegate.takeImageWithCamera(mockMethodCall, mockResult);
+
+ verify(mockActivity)
+ .startActivityForResult(
+ any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA));
+ }
+
+ @Test
+ public void
+ takeImageWithCamera_WhenHasCameraPermission_AndNoActivityToHandleCameraIntent_FinishesWithNoCamerasAvailableError() {
+ when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true);
when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(false);
ImagePickerDelegate delegate = createDelegate();
@@ -158,6 +183,7 @@
@Test
public void takeImageWithCamera_WritesImageToCacheDirectory() {
+ when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true);
when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
MockedStatic<File> mockStaticFile = Mockito.mockStatic(File.class);
@@ -165,16 +191,57 @@
.when(() -> File.createTempFile(any(), any(), any()))
.thenReturn(new File("/tmpfile"));
- try {
- ImagePickerDelegate delegate = createDelegate();
- delegate.takeImageWithCamera(mockMethodCall, mockResult);
+ ImagePickerDelegate delegate = createDelegate();
+ delegate.takeImageWithCamera(mockMethodCall, mockResult);
- mockStaticFile.verify(
- () -> File.createTempFile(any(), eq(".jpg"), eq(new File("/image_picker_cache"))),
- times(1));
- } finally {
- mockStaticFile.close();
- }
+ mockStaticFile.verify(
+ () -> File.createTempFile(any(), eq(".jpg"), eq(new File("/image_picker_cache"))),
+ times(1));
+ }
+
+ @Test
+ public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithError() {
+ ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+
+ delegate.onRequestPermissionsResult(
+ ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION,
+ new String[] {Manifest.permission.CAMERA},
+ new int[] {PackageManager.PERMISSION_DENIED});
+
+ verify(mockResult).error("camera_access_denied", "The user did not allow camera access.", null);
+ verifyNoMoreInteractions(mockResult);
+ }
+
+ @Test
+ public void
+ onRequestTakeVideoPermissionsResult_WhenCameraPermissionGranted_LaunchesTakeVideoWithCameraIntent() {
+ when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
+
+ ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+ delegate.onRequestPermissionsResult(
+ ImagePickerDelegate.REQUEST_CAMERA_VIDEO_PERMISSION,
+ new String[] {Manifest.permission.CAMERA},
+ new int[] {PackageManager.PERMISSION_GRANTED});
+
+ verify(mockActivity)
+ .startActivityForResult(
+ any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA));
+ }
+
+ @Test
+ public void
+ onRequestTakeImagePermissionsResult_WhenCameraPermissionGranted_LaunchesTakeWithCameraIntent() {
+ when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true);
+
+ ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
+ delegate.onRequestPermissionsResult(
+ ImagePickerDelegate.REQUEST_CAMERA_IMAGE_PERMISSION,
+ new String[] {Manifest.permission.CAMERA},
+ new int[] {PackageManager.PERMISSION_GRANTED});
+
+ verify(mockActivity)
+ .startActivityForResult(
+ any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA));
}
@Test
@@ -295,6 +362,7 @@
null,
null,
cache,
+ mockPermissionManager,
mockIntentResolver,
mockFileUriResolver,
mockFileUtils);
@@ -303,11 +371,12 @@
private ImagePickerDelegate createDelegateWithPendingResultAndMethodCall() {
return new ImagePickerDelegate(
mockActivity,
- new File("/image_picker_cache"),
+ null,
mockImageResizer,
mockResult,
mockMethodCall,
cache,
+ mockPermissionManager,
mockIntentResolver,
mockFileUriResolver,
mockFileUtils);
diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml
index 4ca29b4..bf42015 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.8.0+2
+version: 0.8.0+3
environment:
sdk: ">=2.12.0 <3.0.0"