顾海波

【需求】适配鸿蒙,安卓数量选择

... ... @@ -112,6 +112,9 @@ class ImagePicker {
/// image types such as JPEG and on Android PNG and WebP, too. If compression is not
/// supported for the image that is picked, a warning message will be logged.
///
/// The `limit` parameter modifies the maximum number of images that can be selected.
/// This value may be ignored by platforms that cannot support it.
///
/// Use `requestFullMetadata` (defaults to `true`) to control how much additional
/// information the plugin tries to get.
/// If `requestFullMetadata` is set to `true`, the plugin tries to get the full
... ... @@ -128,6 +131,7 @@ class ImagePicker {
double? maxWidth,
double? maxHeight,
int? imageQuality,
int? limit,
bool requestFullMetadata = true,
}) {
final ImageOptions imageOptions = ImageOptions.createAndValidate(
... ... @@ -138,8 +142,9 @@ class ImagePicker {
);
return platform.getMultiImageWithOptions(
options: MultiImagePickerOptions(
options: MultiImagePickerOptions.createAndValidate(
imageOptions: imageOptions,
limit: limit,
),
);
}
... ... @@ -186,7 +191,7 @@ class ImagePicker {
bool requestFullMetadata = true,
}) async {
final List<XFile> listMedia = await platform.getMedia(
options: MediaOptions(
options: MediaOptions.createAndValidate(
imageOptions: ImageOptions.createAndValidate(
maxHeight: maxHeight,
maxWidth: maxWidth,
... ... @@ -223,6 +228,9 @@ class ImagePicker {
/// image types such as JPEG and on Android PNG and WebP, too. If compression is not
/// supported for the image that is picked, a warning message will be logged.
///
/// The `limit` parameter modifies the maximum number of media that can be selected.
/// This value may be ignored by platforms that cannot support it.
///
/// Use `requestFullMetadata` (defaults to `true`) to control how much additional
/// information the plugin tries to get.
/// If `requestFullMetadata` is set to `true`, the plugin tries to get the full
... ... @@ -239,10 +247,11 @@ class ImagePicker {
double? maxWidth,
double? maxHeight,
int? imageQuality,
int? limit,
bool requestFullMetadata = true,
}) {
return platform.getMedia(
options: MediaOptions(
options: MediaOptions.createAndValidate(
allowMultiple: true,
imageOptions: ImageOptions.createAndValidate(
maxHeight: maxHeight,
... ... @@ -250,6 +259,7 @@ class ImagePicker {
imageQuality: imageQuality,
requestFullMetadata: requestFullMetadata,
),
limit: limit,
),
);
}
... ...
... ... @@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.android.tools.build:gradle:8.5.1'
}
}
... ... @@ -26,22 +26,22 @@ android {
if (project.android.hasProperty("namespace")) {
namespace 'io.flutter.plugins.imagepicker'
}
compileSdkVersion 33
compileSdk 34
defaultConfig {
minSdkVersion 16
minSdkVersion 19
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
checkAllWarnings true
warningsAsErrors true
disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency'
disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency', 'NewerVersionAvailable'
}
dependencies {
implementation 'androidx.core:core:1.10.1'
implementation 'androidx.annotation:annotation:1.3.0'
implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.activity:activity:1.7.2'
implementation 'androidx.core:core:1.13.1'
implementation 'androidx.annotation:annotation:1.8.2'
implementation 'androidx.exifinterface:exifinterface:1.3.7'
implementation 'androidx.activity:activity:1.9.1'
// org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions.
// See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
... ...
... ... @@ -4,47 +4,138 @@
package io.flutter.plugins.imagepicker;
import android.util.Log;
import androidx.exifinterface.media.ExifInterface;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
class ExifDataCopier {
void copyExif(String filePathOri, String filePathDest) {
try {
ExifInterface oldExif = new ExifInterface(filePathOri);
ExifInterface newExif = new ExifInterface(filePathDest);
List<String> attributes =
Arrays.asList(
"FNumber",
"ExposureTime",
"ISOSpeedRatings",
"GPSAltitude",
"GPSAltitudeRef",
"FocalLength",
"GPSDateStamp",
"WhiteBalance",
"GPSProcessingMethod",
"GPSTimeStamp",
"DateTime",
"Flash",
"GPSLatitude",
"GPSLatitudeRef",
"GPSLongitude",
"GPSLongitudeRef",
"Make",
"Model",
"Orientation");
for (String attribute : attributes) {
setIfNotNull(oldExif, newExif, attribute);
}
newExif.saveAttributes();
} catch (Exception ex) {
Log.e("ExifDataCopier", "Error preserving Exif data on selected image: " + ex);
/**
* Copies all exif data not related to image structure and orientation tag. Data not related to
* image structure consists of category II (Shooting condition related metadata) and category III
* (Metadata storing other information) tags. Category I tags are not copied because they may be
* invalidated as a result of resizing. The exception is the orientation tag which is known to not
* be invalidated and is crucial for proper display of the image.
*
* <p>The categories mentioned refer to standard "CIPA DC-008-Translation-2012 Exchangeable image
* file format for digital still cameras: Exif Version 2.3"
* https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf. Version 2.3 has been chosen because
* {@code ExifInterface} is based on it.
*/
void copyExif(ExifInterface oldExif, ExifInterface newExif) throws IOException {
@SuppressWarnings("deprecation")
List<String> attributes =
Arrays.asList(
ExifInterface.TAG_IMAGE_DESCRIPTION,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_SOFTWARE,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_ARTIST,
ExifInterface.TAG_COPYRIGHT,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_F_NUMBER,
ExifInterface.TAG_EXPOSURE_PROGRAM,
ExifInterface.TAG_SPECTRAL_SENSITIVITY,
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
ExifInterface.TAG_ISO_SPEED_RATINGS,
ExifInterface.TAG_OECF,
ExifInterface.TAG_SENSITIVITY_TYPE,
ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
ExifInterface.TAG_ISO_SPEED,
ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY,
ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ,
ExifInterface.TAG_EXIF_VERSION,
ExifInterface.TAG_DATETIME_ORIGINAL,
ExifInterface.TAG_DATETIME_DIGITIZED,
ExifInterface.TAG_OFFSET_TIME,
ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
ExifInterface.TAG_APERTURE_VALUE,
ExifInterface.TAG_BRIGHTNESS_VALUE,
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
ExifInterface.TAG_MAX_APERTURE_VALUE,
ExifInterface.TAG_SUBJECT_DISTANCE,
ExifInterface.TAG_METERING_MODE,
ExifInterface.TAG_LIGHT_SOURCE,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_MAKER_NOTE,
ExifInterface.TAG_USER_COMMENT,
ExifInterface.TAG_SUBSEC_TIME,
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
ExifInterface.TAG_FLASHPIX_VERSION,
ExifInterface.TAG_FLASH_ENERGY,
ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
ExifInterface.TAG_EXPOSURE_INDEX,
ExifInterface.TAG_SENSING_METHOD,
ExifInterface.TAG_FILE_SOURCE,
ExifInterface.TAG_SCENE_TYPE,
ExifInterface.TAG_CFA_PATTERN,
ExifInterface.TAG_CUSTOM_RENDERED,
ExifInterface.TAG_EXPOSURE_MODE,
ExifInterface.TAG_WHITE_BALANCE,
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
ExifInterface.TAG_SCENE_CAPTURE_TYPE,
ExifInterface.TAG_GAIN_CONTROL,
ExifInterface.TAG_CONTRAST,
ExifInterface.TAG_SATURATION,
ExifInterface.TAG_SHARPNESS,
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
ExifInterface.TAG_IMAGE_UNIQUE_ID,
ExifInterface.TAG_CAMERA_OWNER_NAME,
ExifInterface.TAG_BODY_SERIAL_NUMBER,
ExifInterface.TAG_LENS_SPECIFICATION,
ExifInterface.TAG_LENS_MAKE,
ExifInterface.TAG_LENS_MODEL,
ExifInterface.TAG_LENS_SERIAL_NUMBER,
ExifInterface.TAG_GPS_VERSION_ID,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_GPS_SATELLITES,
ExifInterface.TAG_GPS_STATUS,
ExifInterface.TAG_GPS_MEASURE_MODE,
ExifInterface.TAG_GPS_DOP,
ExifInterface.TAG_GPS_SPEED_REF,
ExifInterface.TAG_GPS_SPEED,
ExifInterface.TAG_GPS_TRACK_REF,
ExifInterface.TAG_GPS_TRACK,
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
ExifInterface.TAG_GPS_IMG_DIRECTION,
ExifInterface.TAG_GPS_MAP_DATUM,
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
ExifInterface.TAG_GPS_DEST_LATITUDE,
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
ExifInterface.TAG_GPS_DEST_LONGITUDE,
ExifInterface.TAG_GPS_DEST_BEARING_REF,
ExifInterface.TAG_GPS_DEST_BEARING,
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
ExifInterface.TAG_GPS_DEST_DISTANCE,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
ExifInterface.TAG_GPS_AREA_INFORMATION,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_DIFFERENTIAL,
ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
ExifInterface.TAG_INTEROPERABILITY_INDEX,
ExifInterface.TAG_ORIENTATION);
for (String attribute : attributes) {
setIfNotNull(oldExif, newExif, attribute);
}
newExif.saveAttributes();
}
private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) {
... ...
... ... @@ -7,6 +7,7 @@ package io.flutter.plugins.imagepicker;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
... ... @@ -294,10 +295,12 @@ public class ImagePickerDelegate
private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOptions) {
Intent pickMediaIntent;
if (generalOptions.getUsePhotoPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (generalOptions.getUsePhotoPicker()) {
if (generalOptions.getAllowMultiple()) {
int limit = ImagePickerUtils.getLimitFromOption(generalOptions);
pickMediaIntent =
new ActivityResultContracts.PickMultipleVisualMedia()
new ActivityResultContracts.PickMultipleVisualMedia(limit)
.createIntent(
activity,
new PickVisualMediaRequest.Builder()
... ... @@ -319,9 +322,7 @@ public class ImagePickerDelegate
pickMediaIntent.setType("*/*");
String[] mimeTypes = {"video/*", "image/*"};
pickMediaIntent.putExtra("CONTENT_TYPE", mimeTypes);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple());
}
pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple());
}
activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY);
}
... ... @@ -340,7 +341,7 @@ public class ImagePickerDelegate
private void launchPickVideoFromGalleryIntent(Boolean usePhotoPicker) {
Intent pickVideoIntent;
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (usePhotoPicker) {
pickVideoIntent =
new ActivityResultContracts.PickVisualMedia()
.createIntent(
... ... @@ -403,7 +404,7 @@ public class ImagePickerDelegate
} catch (ActivityNotFoundException e) {
try {
// If we can't delete the file again here, there's not really anything we can do about it.
//noinspection ResultOfMethodCallIgnored
// noinspection ResultOfMethodCallIgnored
videoFile.delete();
} catch (SecurityException exception) {
exception.printStackTrace();
... ... @@ -427,18 +428,19 @@ public class ImagePickerDelegate
public void chooseMultiImageFromGallery(
@NonNull ImageSelectionOptions options,
boolean usePhotoPicker,
int limit,
@NonNull Messages.Result<List<String>> result) {
if (!setPendingOptionsAndResult(options, null, result)) {
finishWithAlreadyActiveError(result);
return;
}
launchMultiPickImageFromGalleryIntent(usePhotoPicker);
launchMultiPickImageFromGalleryIntent(usePhotoPicker, limit);
}
private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) {
Intent pickImageIntent;
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (usePhotoPicker) {
pickImageIntent =
new ActivityResultContracts.PickVisualMedia()
.createIntent(
... ... @@ -453,11 +455,11 @@ public class ImagePickerDelegate
activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY);
}
private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker) {
private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker, int limit) {
Intent pickMultiImageIntent;
if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (usePhotoPicker) {
pickMultiImageIntent =
new ActivityResultContracts.PickMultipleVisualMedia()
new ActivityResultContracts.PickMultipleVisualMedia(limit)
.createIntent(
activity,
new PickVisualMediaRequest.Builder()
... ... @@ -466,9 +468,7 @@ public class ImagePickerDelegate
} else {
pickMultiImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
pickMultiImageIntent.setType("image/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
pickMultiImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
pickMultiImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
activity.startActivityForResult(
pickMultiImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY);
... ... @@ -515,7 +515,7 @@ public class ImagePickerDelegate
} catch (ActivityNotFoundException e) {
try {
// If we can't delete the file again here, there's not really anything we can do about it.
//noinspection ResultOfMethodCallIgnored
// noinspection ResultOfMethodCallIgnored
imageFile.delete();
} catch (SecurityException exception) {
exception.printStackTrace();
... ... @@ -549,10 +549,14 @@ public class ImagePickerDelegate
private void grantUriPermissions(Intent intent, Uri imageUri) {
PackageManager packageManager = activity.getPackageManager();
// TODO(stuartmorgan): Add new codepath: https://github.com/flutter/flutter/issues/121816
@SuppressWarnings("deprecation")
List<ResolveInfo> compatibleActivities =
packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
List<ResolveInfo> compatibleActivities;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
compatibleActivities =
packageManager.queryIntentActivities(
intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
} else {
compatibleActivities = queryIntentActivitiesPreApi33(packageManager, intent);
}
for (ResolveInfo info : compatibleActivities) {
activity.grantUriPermission(
... ... @@ -562,6 +566,12 @@ public class ImagePickerDelegate
}
}
@SuppressWarnings("deprecation")
private static List<ResolveInfo> queryIntentActivitiesPreApi33(
PackageManager packageManager, Intent intent) {
return packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
}
@Override
public boolean onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
... ... @@ -628,10 +638,57 @@ public class ImagePickerDelegate
return true;
}
@Nullable
private ArrayList<MediaPath> getPathsFromIntent(@NonNull Intent data, boolean includeMimeType) {
ArrayList<MediaPath> paths = new ArrayList<>();
Uri uri = data.getData();
// On several pre-Android 13 devices using Android Photo Picker, the Uri from getData() could
// be null.
if (uri == null) {
ClipData clipData = data.getClipData();
// If data.getData() and data.getClipData() are both null, we are in an error state. By
// convention we return null from here, and then finish with an error from the corresponding
// handler.
if (clipData == null) {
return null;
}
for (int i = 0; i < data.getClipData().getItemCount(); i++) {
uri = data.getClipData().getItemAt(i).getUri();
// Same error state as above.
if (uri == null) {
return null;
}
String path = fileUtils.getPathFromUri(activity, uri);
// Again, same error state as above.
if (path == null) {
return null;
}
String mimeType = includeMimeType ? activity.getContentResolver().getType(uri) : null;
paths.add(new MediaPath(path, mimeType));
}
} else {
String path = fileUtils.getPathFromUri(activity, uri);
if (path == null) {
return null;
}
paths.add(new MediaPath(path, null));
}
return paths;
}
private void handleChooseImageResult(int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK && data != null) {
String path = fileUtils.getPathFromUri(activity, data.getData());
handleImageResult(path, false);
ArrayList<MediaPath> paths = getPathsFromIntent(data, false);
// If there's no valid Uri, return an error
if (paths == null) {
finishWithError("no_valid_image_uri", "Cannot find the selected image.");
return;
}
handleMediaResult(paths);
return;
}
... ... @@ -659,17 +716,13 @@ public class ImagePickerDelegate
private void handleChooseMediaResult(int resultCode, Intent intent) {
if (resultCode == Activity.RESULT_OK && intent != null) {
ArrayList<MediaPath> paths = new ArrayList<>();
if (intent.getClipData() != null) {
for (int i = 0; i < intent.getClipData().getItemCount(); i++) {
Uri uri = intent.getClipData().getItemAt(i).getUri();
String path = fileUtils.getPathFromUri(activity, uri);
String mimeType = activity.getContentResolver().getType(uri);
paths.add(new MediaPath(path, mimeType));
}
} else {
paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null));
ArrayList<MediaPath> paths = getPathsFromIntent(intent, true);
// If there's no valid Uri, return an error
if (paths == null) {
finishWithError("no_valid_media_uri", "Cannot find the selected media.");
return;
}
handleMediaResult(paths);
return;
}
... ... @@ -680,17 +733,14 @@ public class ImagePickerDelegate
private void handleChooseMultiImageResult(int resultCode, Intent intent) {
if (resultCode == Activity.RESULT_OK && intent != null) {
ArrayList<MediaPath> paths = new ArrayList<>();
if (intent.getClipData() != null) {
for (int i = 0; i < intent.getClipData().getItemCount(); i++) {
paths.add(
new MediaPath(
fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()),
null));
}
} else {
paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null));
ArrayList<MediaPath> paths = getPathsFromIntent(intent, false);
// If there's no valid Uri, return an error
if (paths == null) {
finishWithError(
"missing_valid_image_uri", "Cannot find at least one of the selected images.");
return;
}
handleMediaResult(paths);
return;
}
... ... @@ -701,8 +751,14 @@ public class ImagePickerDelegate
private void handleChooseVideoResult(int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK && data != null) {
String path = fileUtils.getPathFromUri(activity, data.getData());
handleVideoResult(path);
ArrayList<MediaPath> paths = getPathsFromIntent(data, false);
// If there's no valid Uri, return an error
if (paths == null || paths.size() < 1) {
finishWithError("no_valid_video_uri", "Cannot find the selected video.");
return;
}
finishWithSuccess(paths.get(0).path);
return;
}
... ... @@ -733,7 +789,7 @@ public class ImagePickerDelegate
localPendingCameraMediaUrl != null
? localPendingCameraMediaUrl
: Uri.parse(cache.retrievePendingCameraMediaUriPath()),
this::handleVideoResult);
this::finishWithSuccess);
return;
}
... ... @@ -796,10 +852,6 @@ public class ImagePickerDelegate
}
}
private void handleVideoResult(String path) {
finishWithSuccess(path);
}
private boolean setPendingOptionsAndResult(
@Nullable ImageSelectionOptions imageOptions,
@Nullable VideoSelectionOptions videoOptions,
... ...
... ... @@ -18,7 +18,6 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugins.imagepicker.Messages.CacheRetrievalResult;
import io.flutter.plugins.imagepicker.Messages.FlutterError;
import io.flutter.plugins.imagepicker.Messages.GeneralOptions;
... ... @@ -117,7 +116,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
final Activity activity,
final BinaryMessenger messenger,
final ImagePickerApi handler,
final PluginRegistry.Registrar registrar,
final ActivityPluginBinding activityBinding) {
this.application = application;
this.activity = activity;
... ... @@ -125,20 +123,14 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
this.messenger = messenger;
delegate = constructDelegate(activity);
ImagePickerApi.setup(messenger, handler);
ImagePickerApi.setUp(messenger, handler);
observer = new LifeCycleObserver(activity);
if (registrar != null) {
// 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);
}
// V2 embedding setup for activity listeners.
activityBinding.addActivityResultListener(delegate);
activityBinding.addRequestPermissionsResultListener(delegate);
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
lifecycle.addObserver(observer);
}
// Only invoked by {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing.
... ... @@ -159,7 +151,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
lifecycle = null;
}
ImagePickerApi.setup(messenger, null);
ImagePickerApi.setUp(messenger, null);
if (application != null) {
application.unregisterActivityLifecycleCallbacks(observer);
... ... @@ -183,20 +175,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
private FlutterPluginBinding pluginBinding;
ActivityState activityState;
@SuppressWarnings("deprecation")
public static void registerWith(
@NonNull io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
if (registrar.activity() == null) {
// If a background flutter view tries to register the plugin, there will be no activity from the registrar,
// we stop the registering process immediately because the ImagePicker requires an activity.
return;
}
Activity activity = registrar.activity();
Application application = (Application) (registrar.context().getApplicationContext());
ImagePickerPlugin plugin = new ImagePickerPlugin();
plugin.setup(registrar.messenger(), application, activity, registrar, null);
}
/**
* Default constructor for the plugin.
*
... ... @@ -231,7 +209,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
pluginBinding.getBinaryMessenger(),
(Application) pluginBinding.getApplicationContext(),
binding.getActivity(),
null,
binding);
}
... ... @@ -254,10 +231,8 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
final BinaryMessenger messenger,
final Application application,
final Activity activity,
final PluginRegistry.Registrar registrar,
final ActivityPluginBinding activityBinding) {
activityState =
new ActivityState(application, activity, messenger, this, registrar, activityBinding);
activityState = new ActivityState(application, activity, messenger, this, activityBinding);
}
private void tearDown() {
... ... @@ -317,7 +292,10 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
setCameraDevice(delegate, source);
if (generalOptions.getAllowMultiple()) {
delegate.chooseMultiImageFromGallery(options, generalOptions.getUsePhotoPicker(), result);
int limit = ImagePickerUtils.getLimitFromOption(generalOptions);
delegate.chooseMultiImageFromGallery(
options, generalOptions.getUsePhotoPicker(), limit, result);
} else {
switch (source.getType()) {
case GALLERY:
... ...
... ... @@ -5,10 +5,13 @@
package io.flutter.plugins.imagepicker;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.MediaStore;
import androidx.activity.result.contract.ActivityResultContracts;
import java.util.Arrays;
final class ImagePickerUtils {
... ... @@ -16,10 +19,15 @@ final class ImagePickerUtils {
private static boolean isPermissionPresentInManifest(Context context, String permissionName) {
try {
PackageManager packageManager = context.getPackageManager();
// TODO(stuartmorgan): Add new codepath: https://github.com/flutter/flutter/issues/121816
@SuppressWarnings("deprecation")
PackageInfo packageInfo =
packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
PackageInfo packageInfo;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageInfo =
packageManager.getPackageInfo(
context.getPackageName(),
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
} else {
packageInfo = getPermissionsPackageInfoPreApi33(packageManager, context.getPackageName());
}
String[] requestedPermissions = packageInfo.requestedPermissions;
return Arrays.asList(requestedPermissions).contains(permissionName);
... ... @@ -29,6 +37,13 @@ final class ImagePickerUtils {
}
}
@SuppressWarnings("deprecation")
private static PackageInfo getPermissionsPackageInfoPreApi33(
PackageManager packageManager, String packageName)
throws PackageManager.NameNotFoundException {
return packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
}
/**
* 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.
... ... @@ -42,4 +57,32 @@ final class ImagePickerUtils {
boolean greatOrEqualM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
return greatOrEqualM && isPermissionPresentInManifest(context, Manifest.permission.CAMERA);
}
/**
* The system photo picker has a maximum limit of selectable items returned by
* [MediaStore.getPickImagesMaxLimit()] On devices supporting picker provided via
* [ACTION_SYSTEM_FALLBACK_PICK_IMAGES], the limit may be ignored if it's higher than the allowed
* limit. On devices not supporting the photo picker, the limit is ignored.
*
* @see MediaStore.EXTRA_PICK_IMAGES_MAX
*/
@SuppressLint({"NewApi", "ClassVerificationFailure"})
static int getMaxItems() {
if (ActivityResultContracts.PickVisualMedia.isSystemPickerAvailable$activity_release()) {
return MediaStore.getPickImagesMaxLimit();
} else {
return Integer.MAX_VALUE;
}
}
static int getLimitFromOption(Messages.GeneralOptions generalOptions) {
Long limit = generalOptions.getLimit();
int effectiveLimit = getMaxItems();
if (limit != null && limit < effectiveLimit) {
effectiveLimit = Math.toIntExact(limit);
}
return effectiveLimit;
}
}
... ...
... ... @@ -10,7 +10,9 @@ import android.graphics.BitmapFactory;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.SizeFCompat;
import androidx.exifinterface.media.ExifInterface;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
... ... @@ -81,47 +83,34 @@ class ImageResizer {
}
private SizeFCompat calculateTargetSize(
@NonNull Double originalWidth,
@NonNull Double originalHeight,
double originalWidth,
double originalHeight,
@Nullable Double maxWidth,
@Nullable Double maxHeight) {
double aspectRatio = originalWidth / originalHeight;
boolean hasMaxWidth = maxWidth != null;
boolean hasMaxHeight = maxHeight != null;
Double width = hasMaxWidth ? Math.min(originalWidth, maxWidth) : originalWidth;
Double height = hasMaxHeight ? Math.min(originalHeight, maxHeight) : originalHeight;
double width = hasMaxWidth ? Math.min(originalWidth, Math.round(maxWidth)) : originalWidth;
double height = hasMaxHeight ? Math.min(originalHeight, Math.round(maxHeight)) : originalHeight;
boolean shouldDownscaleWidth = hasMaxWidth && maxWidth < originalWidth;
boolean shouldDownscaleHeight = hasMaxHeight && maxHeight < originalHeight;
boolean shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight;
if (shouldDownscale) {
double downscaledWidth = (height / originalHeight) * originalWidth;
double downscaledHeight = (width / originalWidth) * originalHeight;
if (width < height) {
if (!hasMaxWidth) {
width = downscaledWidth;
} else {
height = downscaledHeight;
}
} else if (height < width) {
if (!hasMaxHeight) {
height = downscaledHeight;
} else {
width = downscaledWidth;
}
double WidthForMaxHeight = height * aspectRatio;
double heightForMaxWidth = width / aspectRatio;
if (heightForMaxWidth > height) {
width = (double) Math.round(WidthForMaxHeight);
} else {
if (originalWidth < originalHeight) {
width = downscaledWidth;
} else if (originalHeight < originalWidth) {
height = downscaledHeight;
}
height = (double) Math.round(heightForMaxWidth);
}
}
return new SizeFCompat(width.floatValue(), height.floatValue());
return new SizeFCompat((float) width, (float) height);
}
private File createFile(File externalFilesDirectory, String child) {
... ... @@ -137,10 +126,15 @@ class ImageResizer {
}
private void copyExif(String filePathOri, String filePathDest) {
exifDataCopier.copyExif(filePathOri, filePathDest);
try {
exifDataCopier.copyExif(new ExifInterface(filePathOri), new ExifInterface(filePathDest));
} catch (Exception ex) {
Log.e("ImageResizer", "Error preserving Exif data on selected image: " + ex);
}
}
private SizeFCompat readFileDimensions(String path) {
@VisibleForTesting
SizeFCompat readFileDimensions(String path) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
decodeFile(path, options);
... ...
... ... @@ -6,6 +6,9 @@
package io.flutter.plugins.imagepicker;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.CLASS;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
... ... @@ -14,6 +17,8 @@ import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MessageCodec;
import io.flutter.plugin.common.StandardMessageCodec;
import java.io.ByteArrayOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
... ... @@ -55,6 +60,10 @@ public class Messages {
return errorList;
}
@Target(METHOD)
@Retention(CLASS)
@interface CanIgnoreReturnValue {}
public enum SourceCamera {
REAR(0),
FRONT(1);
... ... @@ -116,6 +125,16 @@ public class Messages {
this.usePhotoPicker = setterArg;
}
private @Nullable Long limit;
public @Nullable Long getLimit() {
return limit;
}
public void setLimit(@Nullable Long setterArg) {
this.limit = setterArg;
}
/** Constructor is non-public to enforce null safety; use Builder. */
GeneralOptions() {}
... ... @@ -123,6 +142,7 @@ public class Messages {
private @Nullable Boolean allowMultiple;
@CanIgnoreReturnValue
public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) {
this.allowMultiple = setterArg;
return this;
... ... @@ -130,24 +150,35 @@ public class Messages {
private @Nullable Boolean usePhotoPicker;
@CanIgnoreReturnValue
public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) {
this.usePhotoPicker = setterArg;
return this;
}
private @Nullable Long limit;
@CanIgnoreReturnValue
public @NonNull Builder setLimit(@Nullable Long setterArg) {
this.limit = setterArg;
return this;
}
public @NonNull GeneralOptions build() {
GeneralOptions pigeonReturn = new GeneralOptions();
pigeonReturn.setAllowMultiple(allowMultiple);
pigeonReturn.setUsePhotoPicker(usePhotoPicker);
pigeonReturn.setLimit(limit);
return pigeonReturn;
}
}
@NonNull
ArrayList<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(2);
ArrayList<Object> toListResult = new ArrayList<Object>(3);
toListResult.add(allowMultiple);
toListResult.add(usePhotoPicker);
toListResult.add(limit);
return toListResult;
}
... ... @@ -157,6 +188,9 @@ public class Messages {
pigeonResult.setAllowMultiple((Boolean) allowMultiple);
Object usePhotoPicker = list.get(1);
pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker);
Object limit = list.get(2);
pigeonResult.setLimit(
(limit == null) ? null : ((limit instanceof Integer) ? (Integer) limit : (Long) limit));
return pigeonResult;
}
}
... ... @@ -214,6 +248,7 @@ public class Messages {
private @Nullable Double maxWidth;
@CanIgnoreReturnValue
public @NonNull Builder setMaxWidth(@Nullable Double setterArg) {
this.maxWidth = setterArg;
return this;
... ... @@ -221,6 +256,7 @@ public class Messages {
private @Nullable Double maxHeight;
@CanIgnoreReturnValue
public @NonNull Builder setMaxHeight(@Nullable Double setterArg) {
this.maxHeight = setterArg;
return this;
... ... @@ -228,6 +264,7 @@ public class Messages {
private @Nullable Long quality;
@CanIgnoreReturnValue
public @NonNull Builder setQuality(@NonNull Long setterArg) {
this.quality = setterArg;
return this;
... ... @@ -288,6 +325,7 @@ public class Messages {
private @Nullable ImageSelectionOptions imageSelectionOptions;
@CanIgnoreReturnValue
public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) {
this.imageSelectionOptions = setterArg;
return this;
... ... @@ -339,6 +377,7 @@ public class Messages {
private @Nullable Long maxDurationSeconds;
@CanIgnoreReturnValue
public @NonNull Builder setMaxDurationSeconds(@Nullable Long setterArg) {
this.maxDurationSeconds = setterArg;
return this;
... ... @@ -407,6 +446,7 @@ public class Messages {
private @Nullable SourceType type;
@CanIgnoreReturnValue
public @NonNull Builder setType(@NonNull SourceType setterArg) {
this.type = setterArg;
return this;
... ... @@ -414,6 +454,7 @@ public class Messages {
private @Nullable SourceCamera camera;
@CanIgnoreReturnValue
public @NonNull Builder setCamera(@Nullable SourceCamera setterArg) {
this.camera = setterArg;
return this;
... ... @@ -438,7 +479,7 @@ public class Messages {
static @NonNull SourceSpecification fromList(@NonNull ArrayList<Object> list) {
SourceSpecification pigeonResult = new SourceSpecification();
Object type = list.get(0);
pigeonResult.setType(type == null ? null : SourceType.values()[(int) type]);
pigeonResult.setType(SourceType.values()[(int) type]);
Object camera = list.get(1);
pigeonResult.setCamera(camera == null ? null : SourceCamera.values()[(int) camera]);
return pigeonResult;
... ... @@ -483,6 +524,7 @@ public class Messages {
private @Nullable String code;
@CanIgnoreReturnValue
public @NonNull Builder setCode(@NonNull String setterArg) {
this.code = setterArg;
return this;
... ... @@ -490,6 +532,7 @@ public class Messages {
private @Nullable String message;
@CanIgnoreReturnValue
public @NonNull Builder setMessage(@Nullable String setterArg) {
this.message = setterArg;
return this;
... ... @@ -578,6 +621,7 @@ public class Messages {
private @Nullable CacheRetrievalType type;
@CanIgnoreReturnValue
public @NonNull Builder setType(@NonNull CacheRetrievalType setterArg) {
this.type = setterArg;
return this;
... ... @@ -585,6 +629,7 @@ public class Messages {
private @Nullable CacheRetrievalError error;
@CanIgnoreReturnValue
public @NonNull Builder setError(@Nullable CacheRetrievalError setterArg) {
this.error = setterArg;
return this;
... ... @@ -592,6 +637,7 @@ public class Messages {
private @Nullable List<String> paths;
@CanIgnoreReturnValue
public @NonNull Builder setPaths(@NonNull List<String> setterArg) {
this.paths = setterArg;
return this;
... ... @@ -618,7 +664,7 @@ public class Messages {
static @NonNull CacheRetrievalResult fromList(@NonNull ArrayList<Object> list) {
CacheRetrievalResult pigeonResult = new CacheRetrievalResult();
Object type = list.get(0);
pigeonResult.setType(type == null ? null : CacheRetrievalType.values()[(int) type]);
pigeonResult.setType(CacheRetrievalType.values()[(int) type]);
Object error = list.get(1);
pigeonResult.setError(
(error == null) ? null : CacheRetrievalError.fromList((ArrayList<Object>) error));
... ... @@ -734,7 +780,7 @@ public class Messages {
return ImagePickerApiCodec.INSTANCE;
}
/** Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. */
static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePickerApi api) {
static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePickerApi api) {
{
BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue();
BasicMessageChannel<Object> channel =
... ...
... ... @@ -67,6 +67,7 @@ class ImagePickerAndroid extends ImagePickerPlatform {
double? maxWidth,
double? maxHeight,
int? imageQuality,
int? limit,
}) {
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
throw ArgumentError.value(
... ... @@ -81,6 +82,10 @@ class ImagePickerAndroid extends ImagePickerPlatform {
throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
}
if (limit != null && limit < 2) {
throw ArgumentError.value(limit, 'limit', 'cannot be lower than 2');
}
return _hostApi.pickImages(
SourceSpecification(type: SourceType.gallery),
ImageSelectionOptions(
... ... @@ -88,7 +93,10 @@ class ImagePickerAndroid extends ImagePickerPlatform {
maxHeight: maxHeight,
quality: imageQuality ?? 100),
GeneralOptions(
allowMultiple: true, usePhotoPicker: useAndroidPhotoPicker),
allowMultiple: true,
usePhotoPicker: useAndroidPhotoPicker,
limit: limit,
),
);
}
... ... @@ -210,15 +218,30 @@ class ImagePickerAndroid extends ImagePickerPlatform {
}
@override
Future<List<XFile>> getMultiImageWithOptions({
MultiImagePickerOptions options = const MultiImagePickerOptions(),
}) async {
final List<dynamic> paths = await _getMultiImagePath(
maxWidth: options.imageOptions.maxWidth,
maxHeight: options.imageOptions.maxHeight,
imageQuality: options.imageOptions.imageQuality,
limit: options.limit,
);
if (paths.isEmpty) {
return <XFile>[];
}
return paths.map((dynamic path) => XFile(path as String)).toList();
}
@override
Future<List<XFile>> getMedia({
required MediaOptions options,
}) async {
return (await _hostApi.pickMedia(
_mediaOptionsToMediaSelectionOptions(options),
GeneralOptions(
allowMultiple: options.allowMultiple,
usePhotoPicker: useAndroidPhotoPicker,
),
_mediaOptionsToGeneralOptions(options),
))
.map((String? path) => XFile(path!))
.toList();
... ... @@ -243,6 +266,7 @@ class ImagePickerAndroid extends ImagePickerPlatform {
final ImageSelectionOptions imageSelectionOptions =
_imageOptionsToImageSelectionOptionsWithValidator(
mediaOptions.imageOptions);
return MediaSelectionOptions(
imageSelectionOptions: imageSelectionOptions,
);
... ... @@ -270,6 +294,29 @@ class ImagePickerAndroid extends ImagePickerPlatform {
quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth);
}
GeneralOptions _mediaOptionsToGeneralOptions(MediaOptions options) {
final bool allowMultiple = options.allowMultiple;
final int? limit = options.limit;
if (!allowMultiple && limit != null) {
throw ArgumentError.value(
allowMultiple,
'allowMultiple',
'cannot be false, when limit is not null',
);
}
if (limit != null && limit < 2) {
throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2');
}
return GeneralOptions(
allowMultiple: allowMultiple,
usePhotoPicker: useAndroidPhotoPicker,
limit: limit,
);
}
@override
Future<LostData> retrieveLostData() async {
final LostDataResponse result = await getLostData();
... ...
... ... @@ -30,16 +30,20 @@ class GeneralOptions {
GeneralOptions({
required this.allowMultiple,
required this.usePhotoPicker,
this.limit,
});
bool allowMultiple;
bool usePhotoPicker;
int? limit;
Object encode() {
return <Object?>[
allowMultiple,
usePhotoPicker,
limit,
];
}
... ... @@ -48,6 +52,7 @@ class GeneralOptions {
return GeneralOptions(
allowMultiple: result[0]! as bool,
usePhotoPicker: result[1]! as bool,
limit: result[2] as int?,
);
}
}
... ... @@ -195,7 +200,7 @@ class CacheRetrievalResult {
CacheRetrievalResult({
required this.type,
this.error,
required this.paths,
this.paths = const <String>[],
});
/// The type of the retrieved data.
... ...
... ... @@ -56,6 +56,7 @@ class ImagePickerOhos extends ImagePickerPlatform {
double? maxWidth,
double? maxHeight,
int? imageQuality,
int? limit,
}) {
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
throw ArgumentError.value(
... ... @@ -70,6 +71,10 @@ class ImagePickerOhos extends ImagePickerPlatform {
throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
}
if (limit != null && limit < 2) {
throw ArgumentError.value(limit, 'limit', 'cannot be lower than 2');
}
return _hostApi.pickImages(
SourceSpecification(type: SourceType.gallery),
ImageSelectionOptions(
... ... @@ -77,7 +82,10 @@ class ImagePickerOhos extends ImagePickerPlatform {
maxHeight: maxHeight,
quality: imageQuality ?? 100),
GeneralOptions(
allowMultiple: true, usePhotoPicker: useOhosPhotoPicker),
allowMultiple: true,
usePhotoPicker: useOhosPhotoPicker,
limit: limit,
),
);
}
... ... @@ -199,18 +207,33 @@ class ImagePickerOhos extends ImagePickerPlatform {
}
@override
Future<List<XFile>> getMultiImageWithOptions({
MultiImagePickerOptions options = const MultiImagePickerOptions(),
}) async {
final List<dynamic> paths = await _getMultiImagePath(
maxWidth: options.imageOptions.maxWidth,
maxHeight: options.imageOptions.maxHeight,
imageQuality: options.imageOptions.imageQuality,
limit: options.limit,
);
if (paths.isEmpty) {
return <XFile>[];
}
return paths.map((dynamic path) => XFile(path as String)).toList();
}
@override
Future<List<XFile>> getMedia({
required MediaOptions options,
}) async {
return (await _hostApi.pickMedia(
_mediaOptionsToMediaSelectionOptions(options),
GeneralOptions(
allowMultiple: options.allowMultiple,
usePhotoPicker: useOhosPhotoPicker,
),
))
.map((String? path) => XFile(path!))
.toList();
_mediaOptionsToGeneralOptions(options),
))
.map((String? path) => XFile(path!))
.toList();
}
@override
... ... @@ -232,6 +255,7 @@ class ImagePickerOhos extends ImagePickerPlatform {
final ImageSelectionOptions imageSelectionOptions =
_imageOptionsToImageSelectionOptionsWithValidator(
mediaOptions.imageOptions);
return MediaSelectionOptions(
imageSelectionOptions: imageSelectionOptions,
);
... ... @@ -259,6 +283,29 @@ class ImagePickerOhos extends ImagePickerPlatform {
quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth);
}
GeneralOptions _mediaOptionsToGeneralOptions(MediaOptions options) {
final bool allowMultiple = options.allowMultiple;
final int? limit = options.limit;
if (!allowMultiple && limit != null) {
throw ArgumentError.value(
allowMultiple,
'allowMultiple',
'cannot be false, when limit is not null',
);
}
if (limit != null && limit < 2) {
throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2');
}
return GeneralOptions(
allowMultiple: allowMultiple,
usePhotoPicker: useOhosPhotoPicker,
limit: limit,
);
}
@override
Future<LostData> retrieveLostData() async {
final LostDataResponse result = await getLostData();
... ...
... ... @@ -30,16 +30,20 @@ class GeneralOptions {
GeneralOptions({
required this.allowMultiple,
required this.usePhotoPicker,
this.limit,
});
bool allowMultiple;
bool usePhotoPicker;
int? limit;
Object encode() {
return <Object?>[
allowMultiple,
usePhotoPicker,
limit,
];
}
... ... @@ -48,6 +52,7 @@ class GeneralOptions {
return GeneralOptions(
allowMultiple: result[0]! as bool,
usePhotoPicker: result[1]! as bool,
limit: result[2] as int?,
);
}
}
... ... @@ -195,7 +200,7 @@ class CacheRetrievalResult {
CacheRetrievalResult({
required this.type,
this.error,
required this.paths,
this.paths = const <String>[],
});
/// The type of the retrieved data.
... ...
... ... @@ -163,7 +163,7 @@ export default class ImagePickerDelegate {
return;
}
this.chooseMedia(generalOptions.getAllowMultiple() ? 9 : 1, 'handleChooseMediaResult')
this.chooseMedia(generalOptions.getAllowMultiple() ? generalOptions.getLimit() : 1, 'handleChooseMediaResult')
}
handleChooseMediaResult(code: number, uris: Array<string>): void {
... ... @@ -279,13 +279,13 @@ export default class ImagePickerDelegate {
}
// 选择多个图片
chooseMultiImagesFromGallery(options: ImageSelectionOptions, usePhotoPicker: boolean, result: Result<ArrayList<string>>): void {
chooseMultiImagesFromGallery(options: ImageSelectionOptions,limit: number, usePhotoPicker: boolean, result: Result<ArrayList<string>>): void {
if (!this.setPendingOptionsAndResult(options, null, result)) {
this.finishWithAlreadyActiveError(result);
return;
}
this.chooseMedia(9, 'handleChooseMediaResult', photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE)
this.chooseMedia(limit, 'handleChooseMediaResult', photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE)
}
// 唤起相机拍照
... ...
... ... @@ -95,7 +95,7 @@ export default class ImagePickerPlugin implements FlutterPlugin, AbilityAware {
this.setCameraDevice(delegate, source);
if (generalOptions.getAllowMultiple()) {
delegate.chooseMultiImagesFromGallery(options, generalOptions.getUsePhotoPicker(), result);
delegate.chooseMultiImagesFromGallery(options,generalOptions.getLimit(), generalOptions.getUsePhotoPicker(), result);
} else {
switch (source.getType()) {
case SourceType.GALLERY: {
... ...
... ... @@ -60,11 +60,14 @@ export class FlutterError extends Error {
class GeneralOptionsBuilder {
setAllowMultiple: (setterArg: boolean) => ESObject
setUsePhotoPicker: (setterArg: boolean) => ESObject
setLimit: (setterArg: number) => ESObject
build: () => ESObject
constructor(setAllowMultiple: (setterArg: boolean) => ESObject, setUsePhotoPicker: (setterArg: boolean) => ESObject, build: () => ESObject) {
constructor(setAllowMultiple: (setterArg: boolean) => ESObject, setUsePhotoPicker: (setterArg: boolean) => ESObject,setLimit: (setterArg: number) => ESObject, build: () => ESObject) {
this.setAllowMultiple = setAllowMultiple
this.setUsePhotoPicker = setUsePhotoPicker
this.setLimit = setLimit
this.build = build
}
}
... ... @@ -72,6 +75,7 @@ class GeneralOptionsBuilder {
export class GeneralOptions {
private allowMultiple: boolean = false;
private usePhotoPicker: boolean = false;
private limit: number = 1;
private constructor() {
}
... ... @@ -98,16 +102,33 @@ export class GeneralOptions {
this.usePhotoPicker = setterArg;
}
getLimit(): number {
return this.limit;
}
setLimit(setterArg: number): void {
if (setterArg == null) {
throw new Error("Nonnull field \"limit\" is null.");
}
this.limit = setterArg;
}
public Builder: ESObject = new GeneralOptionsBuilder((setterArg: boolean) => {
this.allowMultiple = setterArg;
return this;
}, (setterArg: boolean) => {
this.usePhotoPicker = setterArg;
return this;
}, (): ESObject => {
}, (setterArg: number) => {
this.limit = setterArg;
return this;
}, (): ESObject => {
const pigeonReturn: ESObject = new GeneralOptions();
pigeonReturn.setAllowMultiple(this.allowMultiple);
pigeonReturn.setUsePhotoPicker(this.usePhotoPicker);
pigeonReturn.setUsePhotoPicker(this.limit);
return pigeonReturn;
}
)
... ... @@ -116,6 +137,7 @@ export class GeneralOptions {
const toListResult: ArrayList<ESObject> = new ArrayList<ESObject>();
toListResult.add(this.allowMultiple);
toListResult.add(this.usePhotoPicker);
toListResult.add(this.limit);
return toListResult;
}
... ... @@ -125,6 +147,8 @@ export class GeneralOptions {
pigeonResult.setAllowMultiple(allowMultiple);
const usePhotoPicker: ESObject = list[1];
pigeonResult.setUsePhotoPicker(usePhotoPicker);
const limit: ESObject = list[2];
pigeonResult.setLimit(limit);
return pigeonResult;
}
}
... ...
... ... @@ -58,6 +58,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
double? maxHeight,
int? imageQuality,
bool requestFullMetadata = true,
int? limit,
}) {
if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
throw ArgumentError.value(
... ... @@ -79,6 +80,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
'maxHeight': maxHeight,
'imageQuality': imageQuality,
'requestFullMetadata': requestFullMetadata,
'limit': limit,
},
);
}
... ... @@ -244,6 +246,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
maxHeight: options.imageOptions.maxHeight,
imageQuality: options.imageOptions.imageQuality,
requestFullMetadata: options.imageOptions.requestFullMetadata,
limit: options.limit,
);
if (paths == null) {
return <XFile>[];
... ... @@ -263,6 +266,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
'maxImageHeight': imageOptions.maxHeight,
'imageQuality': imageOptions.imageQuality,
'allowMultiple': options.allowMultiple,
'limit': options.limit,
};
final List<XFile>? paths = await _channel
... ... @@ -312,13 +316,10 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
switch (type) {
case kTypeImage:
retrieveType = RetrieveType.image;
break;
case kTypeVideo:
retrieveType = RetrieveType.video;
break;
case kTypeMedia:
retrieveType = RetrieveType.media;
break;
}
PlatformException? exception;
... ...
... ... @@ -13,11 +13,48 @@ class MediaOptions {
const MediaOptions({
this.imageOptions = const ImageOptions(),
required this.allowMultiple,
this.limit,
});
/// Construct a new MediaOptions instance.
///
/// Throws if limit is lower than 2,
/// or allowMultiple is false and limit is not null.
MediaOptions.createAndValidate({
this.imageOptions = const ImageOptions(),
required this.allowMultiple,
this.limit,
}) {
_validate(allowMultiple: allowMultiple, limit: limit);
}
/// Options that will apply to images upon selection.
final ImageOptions imageOptions;
/// Whether to allow for selecting multiple media.
final bool allowMultiple;
/// The maximum number of images to select.
///
/// Default null value means no limit.
/// This value may be ignored by platforms that cannot support it.
final int? limit;
/// Validates that all values are within required ranges.
///
/// Throws if limit is lower than 2,
/// or allowMultiple is false and limit is not null.
static void _validate({required bool allowMultiple, int? limit}) {
if (!allowMultiple && limit != null) {
throw ArgumentError.value(
allowMultiple,
'allowMultiple',
'cannot be false, when limit is not null',
);
}
if (limit != null && limit < 2) {
throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2');
}
}
}
... ...
... ... @@ -6,11 +6,37 @@ import 'image_options.dart';
/// Specifies options for picking multiple images from the device's gallery.
class MultiImagePickerOptions {
/// Creates an instance with the given [imageOptions].
/// Creates an instance with the given [imageOptions] and [limit].
const MultiImagePickerOptions({
this.imageOptions = const ImageOptions(),
this.limit,
});
/// Creates an instance with the given [imageOptions] and [limit].
///
/// Throws if limit is lower than 2.
MultiImagePickerOptions.createAndValidate({
this.imageOptions = const ImageOptions(),
this.limit,
}) {
_validate(limit: limit);
}
/// The image-specific options for picking.
final ImageOptions imageOptions;
/// The maximum number of images to select.
///
/// Default null value means no limit.
/// This value may be ignored by platforms that cannot support it.
final int? limit;
/// Validates that all values are within required ranges.
///
/// Throws if limit is lower than 2.
static void _validate({int? limit}) {
if (limit != null && limit < 2) {
throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2');
}
}
}
... ...