Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/image_picker/image_picker_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 0.8.5+6
## 0.8.6

* Adds Android 13 photo picker functionality if SDK version is at least 33.
* Bumps compileSdkVersion from 31 to 33
* Updates minimum Flutter version to 3.0.
* Fixes names of picked files to match original filenames where possible.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ rootProject.allprojects {
apply plugin: 'com.android.library'

android {
compileSdkVersion 31
compileSdkVersion 33

defaultConfig {
minSdkVersion 16
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Fri Jan 27 08:52:19 CST 2023
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,20 @@ enum CameraDevice {
public class ImagePickerDelegate
implements PluginRegistry.ActivityResultListener,
PluginRegistry.RequestPermissionsResultListener {
@VisibleForTesting
static final int REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY_USING_PHOTO_PICKER = 2341;

@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_MULTI_IMAGE_FROM_GALLERY = 2346;

@VisibleForTesting
static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY_USING_PHOTO_PICKER = 2347;

@VisibleForTesting
static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY_USING_PHOTO_PICKER = 2351;

@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;
Expand Down Expand Up @@ -253,10 +263,18 @@ public void chooseVideoFromGallery(MethodCall methodCall, MethodChannel.Result r
}

private void launchPickVideoFromGalleryIntent() {
Intent pickVideoIntent = new Intent(Intent.ACTION_GET_CONTENT);
boolean isPhotoPickerAvailable = ImagePickerUtils.isPhotoPickerAvailable();

Intent pickVideoIntent =
new Intent(
isPhotoPickerAvailable ? MediaStore.ACTION_PICK_IMAGES : Intent.ACTION_GET_CONTENT);
pickVideoIntent.setType("video/*");
int requestCode =
isPhotoPickerAvailable
? REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY_USING_PHOTO_PICKER
: REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY;

activity.startActivityForResult(pickVideoIntent, REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY);
activity.startActivityForResult(pickVideoIntent, requestCode);
}

public void takeVideoWithCamera(MethodCall methodCall, MethodChannel.Result result) {
Expand Down Expand Up @@ -325,20 +343,38 @@ public void chooseMultiImageFromGallery(MethodCall methodCall, MethodChannel.Res
}

private void launchPickImageFromGalleryIntent() {
Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
boolean isPhotoPickerAvailable = ImagePickerUtils.isPhotoPickerAvailable();
Intent pickImageIntent =
new Intent(
isPhotoPickerAvailable ? MediaStore.ACTION_PICK_IMAGES : Intent.ACTION_GET_CONTENT);
int requestCode =
isPhotoPickerAvailable
? REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY_USING_PHOTO_PICKER
: REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY;
pickImageIntent.setType("image/*");

activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY);
activity.startActivityForResult(pickImageIntent, requestCode);
}

private void launchMultiPickImageFromGalleryIntent() {
Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
boolean isPhotoPickerAvailable = ImagePickerUtils.isPhotoPickerAvailable();
Intent pickImageIntent =
new Intent(
isPhotoPickerAvailable ? MediaStore.ACTION_PICK_IMAGES : Intent.ACTION_GET_CONTENT);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
if (isPhotoPickerAvailable) {
pickImageIntent.putExtra(
MediaStore.EXTRA_PICK_IMAGES_MAX, ImagePickerUtils.getPickImagesMaxLimit());
} else {
pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
}
pickImageIntent.setType("image/*");

activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY);
int requestCode =
isPhotoPickerAvailable
? REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY_USING_PHOTO_PICKER
: REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY;
activity.startActivityForResult(pickImageIntent, requestCode);
}

public void takeImageWithCamera(MethodCall methodCall, MethodChannel.Result result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

package io.flutter.plugins.imagepicker;

import static android.os.ext.SdkExtensions.getExtensionVersion;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.MediaStore;
import java.util.Arrays;

final class ImagePickerUtils {
Expand All @@ -27,6 +30,25 @@ private static boolean isPermissionPresentInManifest(Context context, String per
}
}

static boolean isPhotoPickerAvailable() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return getExtensionVersion(Build.VERSION_CODES.R) >= 2;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return true;
} else {
return false;
}
}

static int getPickImagesMaxLimit() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return MediaStore.getPickImagesMaxLimit();
} else {
return 0;
}
}

/**
* Camera permission need request if it present in manifest, because for M or great for take Photo
* ar Video by intent need it permission, even if the camera permission is not used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@
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.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
public class ImagePickerDelegateTest {
private static final Double WIDTH = 10.0;
private static final Double HEIGHT = 10.0;
Expand Down Expand Up @@ -134,6 +138,8 @@ public void chooseMultiImageFromGallery_WhenPendingResultExists_FinishesWithAlre
verifyNoMoreInteractions(mockResult);
}

@Test
@Config(sdk = 30)
public void
chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
Expand All @@ -147,6 +153,87 @@ public void chooseMultiImageFromGallery_WhenPendingResultExists_FinishesWithAlre
any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY));
}

@Test
@Config(minSdk = 33)
public void
chooseImageFromGallery_WithPhotoPicker_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
.thenReturn(true);

ImagePickerDelegate delegate = createDelegate();
delegate.chooseImageFromGallery(mockMethodCall, mockResult);

verify(mockActivity)
.startActivityForResult(
any(Intent.class),
eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY_USING_PHOTO_PICKER));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Continuing from flutter/plugins#7043 (comment) on the pre-migration version of this PR, and from Discord.)

This is checking what "request code" we passed to startActivityForResult. But the request code doesn't have any effect on what UI the system presents to the user, or anything like that — it's a number we make up for our own internal tracking, so that our onActivityResult can know what to do with the result it gets.

The thing that controls whether the system shows the spiffy new photo picker, or does something else, is the "action" in the Intent. So that would be the thing to inspect if writing this style of unit test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's probably the clearest upstream docs on that — in particular the "Action" subsection: https://developer.android.com/guide/components/intents-filters

Is this something we want want to implement for this PR then? Looks like all the tests currently only check that the intent has the correct tag so I imagine we'd want to refactor those tests to also confirm the correct action.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine we'd want to refactor those tests to also confirm the correct action.

I think that would be a good improvement, but my suggestion for this PR would be to leave the tests for other functionality as they are.

}

@Test
@Config(sdk = 30)
public void
chooseMultiImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
.thenReturn(true);

ImagePickerDelegate delegate = createDelegate();
delegate.chooseMultiImageFromGallery(mockMethodCall, mockResult);

verify(mockActivity)
.startActivityForResult(
any(Intent.class),
eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY));
}

@Test
@Config(minSdk = 33)
public void
chooseMultiImageFromGallery_WithPhotoPicker_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
.thenReturn(true);

ImagePickerDelegate delegate = createDelegate();
delegate.chooseMultiImageFromGallery(mockMethodCall, mockResult);

verify(mockActivity)
.startActivityForResult(
any(Intent.class),
eq(
ImagePickerDelegate
.REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY_USING_PHOTO_PICKER));
}

@Test
@Config(sdk = 30)
public void
chooseVideoFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
.thenReturn(true);

ImagePickerDelegate delegate = createDelegate();
delegate.chooseVideoFromGallery(mockMethodCall, mockResult);

verify(mockActivity)
.startActivityForResult(
any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY));
}

@Test
@Config(minSdk = 33)
public void
chooseVideoFromGallery_WithPhotoPicker_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() {
when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE))
.thenReturn(true);

ImagePickerDelegate delegate = createDelegate();
delegate.chooseVideoFromGallery(mockMethodCall, mockResult);

verify(mockActivity)
.startActivityForResult(
any(Intent.class),
eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY_USING_PHOTO_PICKER));
}

@Test
public void takeImageWithCamera_WhenPendingResultExists_FinishesWithAlreadyActiveError() {
ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
Expand Down Expand Up @@ -350,6 +437,7 @@ public void onActivityResult_WhenTakeImageWithCameraCanceled_FinishesWithNull()
@Test
public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_FinishesWithImagePath() {
ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");

delegate.onActivityResult(
ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
Expand All @@ -362,6 +450,7 @@ public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_Finishes
public void
onActivityResult_WhenImageTakenWithCamera_AndResizeNeeded_FinishesWithScaledImagePath() {
when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH);
when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");

ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
delegate.onActivityResult(
Expand All @@ -375,6 +464,7 @@ public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_Finishes
public void
onActivityResult_WhenVideoTakenWithCamera_AndResizeParametersSupplied_FinishesWithFilePath() {
when(mockMethodCall.argument("maxWidth")).thenReturn(WIDTH);
when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");

ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
delegate.onActivityResult(
Expand All @@ -388,6 +478,7 @@ public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_Finishes
public void
onActivityResult_WhenVideoTakenWithCamera_AndMaxDurationParametersSupplied_FinishesWithFilePath() {
when(mockMethodCall.argument("maxDuration")).thenReturn(MAX_DURATION);
when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");

ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall();
delegate.onActivityResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 31
compileSdkVersion 33
testOptions.unitTests.includeAndroidResources = true

lintOptions {
Expand Down
2 changes: 1 addition & 1 deletion packages/image_picker/image_picker_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: image_picker_android
description: Android implementation of the image_picker plugin.
repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
version: 0.8.5+6
version: 0.8.6

environment:
sdk: ">=2.14.0 <3.0.0"
Expand Down