顾海波

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

@@ -112,6 +112,9 @@ class ImagePicker { @@ -112,6 +112,9 @@ class ImagePicker {
112 /// image types such as JPEG and on Android PNG and WebP, too. If compression is not 112 /// image types such as JPEG and on Android PNG and WebP, too. If compression is not
113 /// supported for the image that is picked, a warning message will be logged. 113 /// supported for the image that is picked, a warning message will be logged.
114 /// 114 ///
  115 + /// The `limit` parameter modifies the maximum number of images that can be selected.
  116 + /// This value may be ignored by platforms that cannot support it.
  117 + ///
115 /// Use `requestFullMetadata` (defaults to `true`) to control how much additional 118 /// Use `requestFullMetadata` (defaults to `true`) to control how much additional
116 /// information the plugin tries to get. 119 /// information the plugin tries to get.
117 /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full 120 /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full
@@ -128,6 +131,7 @@ class ImagePicker { @@ -128,6 +131,7 @@ class ImagePicker {
128 double? maxWidth, 131 double? maxWidth,
129 double? maxHeight, 132 double? maxHeight,
130 int? imageQuality, 133 int? imageQuality,
  134 + int? limit,
131 bool requestFullMetadata = true, 135 bool requestFullMetadata = true,
132 }) { 136 }) {
133 final ImageOptions imageOptions = ImageOptions.createAndValidate( 137 final ImageOptions imageOptions = ImageOptions.createAndValidate(
@@ -138,8 +142,9 @@ class ImagePicker { @@ -138,8 +142,9 @@ class ImagePicker {
138 ); 142 );
139 143
140 return platform.getMultiImageWithOptions( 144 return platform.getMultiImageWithOptions(
141 - options: MultiImagePickerOptions( 145 + options: MultiImagePickerOptions.createAndValidate(
142 imageOptions: imageOptions, 146 imageOptions: imageOptions,
  147 + limit: limit,
143 ), 148 ),
144 ); 149 );
145 } 150 }
@@ -186,7 +191,7 @@ class ImagePicker { @@ -186,7 +191,7 @@ class ImagePicker {
186 bool requestFullMetadata = true, 191 bool requestFullMetadata = true,
187 }) async { 192 }) async {
188 final List<XFile> listMedia = await platform.getMedia( 193 final List<XFile> listMedia = await platform.getMedia(
189 - options: MediaOptions( 194 + options: MediaOptions.createAndValidate(
190 imageOptions: ImageOptions.createAndValidate( 195 imageOptions: ImageOptions.createAndValidate(
191 maxHeight: maxHeight, 196 maxHeight: maxHeight,
192 maxWidth: maxWidth, 197 maxWidth: maxWidth,
@@ -223,6 +228,9 @@ class ImagePicker { @@ -223,6 +228,9 @@ class ImagePicker {
223 /// image types such as JPEG and on Android PNG and WebP, too. If compression is not 228 /// image types such as JPEG and on Android PNG and WebP, too. If compression is not
224 /// supported for the image that is picked, a warning message will be logged. 229 /// supported for the image that is picked, a warning message will be logged.
225 /// 230 ///
  231 + /// The `limit` parameter modifies the maximum number of media that can be selected.
  232 + /// This value may be ignored by platforms that cannot support it.
  233 + ///
226 /// Use `requestFullMetadata` (defaults to `true`) to control how much additional 234 /// Use `requestFullMetadata` (defaults to `true`) to control how much additional
227 /// information the plugin tries to get. 235 /// information the plugin tries to get.
228 /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full 236 /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full
@@ -239,10 +247,11 @@ class ImagePicker { @@ -239,10 +247,11 @@ class ImagePicker {
239 double? maxWidth, 247 double? maxWidth,
240 double? maxHeight, 248 double? maxHeight,
241 int? imageQuality, 249 int? imageQuality,
  250 + int? limit,
242 bool requestFullMetadata = true, 251 bool requestFullMetadata = true,
243 }) { 252 }) {
244 return platform.getMedia( 253 return platform.getMedia(
245 - options: MediaOptions( 254 + options: MediaOptions.createAndValidate(
246 allowMultiple: true, 255 allowMultiple: true,
247 imageOptions: ImageOptions.createAndValidate( 256 imageOptions: ImageOptions.createAndValidate(
248 maxHeight: maxHeight, 257 maxHeight: maxHeight,
@@ -250,6 +259,7 @@ class ImagePicker { @@ -250,6 +259,7 @@ class ImagePicker {
250 imageQuality: imageQuality, 259 imageQuality: imageQuality,
251 requestFullMetadata: requestFullMetadata, 260 requestFullMetadata: requestFullMetadata,
252 ), 261 ),
  262 + limit: limit,
253 ), 263 ),
254 ); 264 );
255 } 265 }
@@ -8,7 +8,7 @@ buildscript { @@ -8,7 +8,7 @@ buildscript {
8 } 8 }
9 9
10 dependencies { 10 dependencies {
11 - classpath 'com.android.tools.build:gradle:7.2.1' 11 + classpath 'com.android.tools.build:gradle:8.5.1'
12 } 12 }
13 } 13 }
14 14
@@ -26,22 +26,22 @@ android { @@ -26,22 +26,22 @@ android {
26 if (project.android.hasProperty("namespace")) { 26 if (project.android.hasProperty("namespace")) {
27 namespace 'io.flutter.plugins.imagepicker' 27 namespace 'io.flutter.plugins.imagepicker'
28 } 28 }
29 - compileSdkVersion 33 29 + compileSdk 34
30 30
31 defaultConfig { 31 defaultConfig {
32 - minSdkVersion 16 32 + minSdkVersion 19
33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
34 } 34 }
35 lintOptions { 35 lintOptions {
36 checkAllWarnings true 36 checkAllWarnings true
37 warningsAsErrors true 37 warningsAsErrors true
38 - disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' 38 + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency', 'NewerVersionAvailable'
39 } 39 }
40 dependencies { 40 dependencies {
41 - implementation 'androidx.core:core:1.10.1'  
42 - implementation 'androidx.annotation:annotation:1.3.0'  
43 - implementation 'androidx.exifinterface:exifinterface:1.3.6'  
44 - implementation 'androidx.activity:activity:1.7.2' 41 + implementation 'androidx.core:core:1.13.1'
  42 + implementation 'androidx.annotation:annotation:1.8.2'
  43 + implementation 'androidx.exifinterface:exifinterface:1.3.7'
  44 + implementation 'androidx.activity:activity:1.9.1'
45 // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. 45 // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions.
46 // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 46 // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7
47 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) 47 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
@@ -4,47 +4,138 @@ @@ -4,47 +4,138 @@
4 4
5 package io.flutter.plugins.imagepicker; 5 package io.flutter.plugins.imagepicker;
6 6
7 -import android.util.Log;  
8 import androidx.exifinterface.media.ExifInterface; 7 import androidx.exifinterface.media.ExifInterface;
  8 +import java.io.IOException;
9 import java.util.Arrays; 9 import java.util.Arrays;
10 import java.util.List; 10 import java.util.List;
11 11
12 class ExifDataCopier { 12 class ExifDataCopier {
13 - void copyExif(String filePathOri, String filePathDest) {  
14 - try {  
15 - ExifInterface oldExif = new ExifInterface(filePathOri);  
16 - ExifInterface newExif = new ExifInterface(filePathDest);  
17 -  
18 - List<String> attributes =  
19 - Arrays.asList(  
20 - "FNumber",  
21 - "ExposureTime",  
22 - "ISOSpeedRatings",  
23 - "GPSAltitude",  
24 - "GPSAltitudeRef",  
25 - "FocalLength",  
26 - "GPSDateStamp",  
27 - "WhiteBalance",  
28 - "GPSProcessingMethod",  
29 - "GPSTimeStamp",  
30 - "DateTime",  
31 - "Flash",  
32 - "GPSLatitude",  
33 - "GPSLatitudeRef",  
34 - "GPSLongitude",  
35 - "GPSLongitudeRef",  
36 - "Make",  
37 - "Model",  
38 - "Orientation");  
39 - for (String attribute : attributes) {  
40 - setIfNotNull(oldExif, newExif, attribute);  
41 - }  
42 -  
43 - newExif.saveAttributes();  
44 -  
45 - } catch (Exception ex) {  
46 - Log.e("ExifDataCopier", "Error preserving Exif data on selected image: " + ex); 13 + /**
  14 + * Copies all exif data not related to image structure and orientation tag. Data not related to
  15 + * image structure consists of category II (Shooting condition related metadata) and category III
  16 + * (Metadata storing other information) tags. Category I tags are not copied because they may be
  17 + * invalidated as a result of resizing. The exception is the orientation tag which is known to not
  18 + * be invalidated and is crucial for proper display of the image.
  19 + *
  20 + * <p>The categories mentioned refer to standard "CIPA DC-008-Translation-2012 Exchangeable image
  21 + * file format for digital still cameras: Exif Version 2.3"
  22 + * https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf. Version 2.3 has been chosen because
  23 + * {@code ExifInterface} is based on it.
  24 + */
  25 + void copyExif(ExifInterface oldExif, ExifInterface newExif) throws IOException {
  26 + @SuppressWarnings("deprecation")
  27 + List<String> attributes =
  28 + Arrays.asList(
  29 + ExifInterface.TAG_IMAGE_DESCRIPTION,
  30 + ExifInterface.TAG_MAKE,
  31 + ExifInterface.TAG_MODEL,
  32 + ExifInterface.TAG_SOFTWARE,
  33 + ExifInterface.TAG_DATETIME,
  34 + ExifInterface.TAG_ARTIST,
  35 + ExifInterface.TAG_COPYRIGHT,
  36 + ExifInterface.TAG_EXPOSURE_TIME,
  37 + ExifInterface.TAG_F_NUMBER,
  38 + ExifInterface.TAG_EXPOSURE_PROGRAM,
  39 + ExifInterface.TAG_SPECTRAL_SENSITIVITY,
  40 + ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
  41 + ExifInterface.TAG_ISO_SPEED_RATINGS,
  42 + ExifInterface.TAG_OECF,
  43 + ExifInterface.TAG_SENSITIVITY_TYPE,
  44 + ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
  45 + ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
  46 + ExifInterface.TAG_ISO_SPEED,
  47 + ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY,
  48 + ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ,
  49 + ExifInterface.TAG_EXIF_VERSION,
  50 + ExifInterface.TAG_DATETIME_ORIGINAL,
  51 + ExifInterface.TAG_DATETIME_DIGITIZED,
  52 + ExifInterface.TAG_OFFSET_TIME,
  53 + ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
  54 + ExifInterface.TAG_OFFSET_TIME_DIGITIZED,
  55 + ExifInterface.TAG_SHUTTER_SPEED_VALUE,
  56 + ExifInterface.TAG_APERTURE_VALUE,
  57 + ExifInterface.TAG_BRIGHTNESS_VALUE,
  58 + ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
  59 + ExifInterface.TAG_MAX_APERTURE_VALUE,
  60 + ExifInterface.TAG_SUBJECT_DISTANCE,
  61 + ExifInterface.TAG_METERING_MODE,
  62 + ExifInterface.TAG_LIGHT_SOURCE,
  63 + ExifInterface.TAG_FLASH,
  64 + ExifInterface.TAG_FOCAL_LENGTH,
  65 + ExifInterface.TAG_MAKER_NOTE,
  66 + ExifInterface.TAG_USER_COMMENT,
  67 + ExifInterface.TAG_SUBSEC_TIME,
  68 + ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
  69 + ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
  70 + ExifInterface.TAG_FLASHPIX_VERSION,
  71 + ExifInterface.TAG_FLASH_ENERGY,
  72 + ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
  73 + ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
  74 + ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
  75 + ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
  76 + ExifInterface.TAG_EXPOSURE_INDEX,
  77 + ExifInterface.TAG_SENSING_METHOD,
  78 + ExifInterface.TAG_FILE_SOURCE,
  79 + ExifInterface.TAG_SCENE_TYPE,
  80 + ExifInterface.TAG_CFA_PATTERN,
  81 + ExifInterface.TAG_CUSTOM_RENDERED,
  82 + ExifInterface.TAG_EXPOSURE_MODE,
  83 + ExifInterface.TAG_WHITE_BALANCE,
  84 + ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
  85 + ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
  86 + ExifInterface.TAG_SCENE_CAPTURE_TYPE,
  87 + ExifInterface.TAG_GAIN_CONTROL,
  88 + ExifInterface.TAG_CONTRAST,
  89 + ExifInterface.TAG_SATURATION,
  90 + ExifInterface.TAG_SHARPNESS,
  91 + ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
  92 + ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
  93 + ExifInterface.TAG_IMAGE_UNIQUE_ID,
  94 + ExifInterface.TAG_CAMERA_OWNER_NAME,
  95 + ExifInterface.TAG_BODY_SERIAL_NUMBER,
  96 + ExifInterface.TAG_LENS_SPECIFICATION,
  97 + ExifInterface.TAG_LENS_MAKE,
  98 + ExifInterface.TAG_LENS_MODEL,
  99 + ExifInterface.TAG_LENS_SERIAL_NUMBER,
  100 + ExifInterface.TAG_GPS_VERSION_ID,
  101 + ExifInterface.TAG_GPS_LATITUDE_REF,
  102 + ExifInterface.TAG_GPS_LATITUDE,
  103 + ExifInterface.TAG_GPS_LONGITUDE_REF,
  104 + ExifInterface.TAG_GPS_LONGITUDE,
  105 + ExifInterface.TAG_GPS_ALTITUDE_REF,
  106 + ExifInterface.TAG_GPS_ALTITUDE,
  107 + ExifInterface.TAG_GPS_TIMESTAMP,
  108 + ExifInterface.TAG_GPS_SATELLITES,
  109 + ExifInterface.TAG_GPS_STATUS,
  110 + ExifInterface.TAG_GPS_MEASURE_MODE,
  111 + ExifInterface.TAG_GPS_DOP,
  112 + ExifInterface.TAG_GPS_SPEED_REF,
  113 + ExifInterface.TAG_GPS_SPEED,
  114 + ExifInterface.TAG_GPS_TRACK_REF,
  115 + ExifInterface.TAG_GPS_TRACK,
  116 + ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
  117 + ExifInterface.TAG_GPS_IMG_DIRECTION,
  118 + ExifInterface.TAG_GPS_MAP_DATUM,
  119 + ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
  120 + ExifInterface.TAG_GPS_DEST_LATITUDE,
  121 + ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
  122 + ExifInterface.TAG_GPS_DEST_LONGITUDE,
  123 + ExifInterface.TAG_GPS_DEST_BEARING_REF,
  124 + ExifInterface.TAG_GPS_DEST_BEARING,
  125 + ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
  126 + ExifInterface.TAG_GPS_DEST_DISTANCE,
  127 + ExifInterface.TAG_GPS_PROCESSING_METHOD,
  128 + ExifInterface.TAG_GPS_AREA_INFORMATION,
  129 + ExifInterface.TAG_GPS_DATESTAMP,
  130 + ExifInterface.TAG_GPS_DIFFERENTIAL,
  131 + ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
  132 + ExifInterface.TAG_INTEROPERABILITY_INDEX,
  133 + ExifInterface.TAG_ORIENTATION);
  134 + for (String attribute : attributes) {
  135 + setIfNotNull(oldExif, newExif, attribute);
47 } 136 }
  137 +
  138 + newExif.saveAttributes();
48 } 139 }
49 140
50 private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) { 141 private static void setIfNotNull(ExifInterface oldExif, ExifInterface newExif, String property) {
@@ -7,6 +7,7 @@ package io.flutter.plugins.imagepicker; @@ -7,6 +7,7 @@ package io.flutter.plugins.imagepicker;
7 import android.Manifest; 7 import android.Manifest;
8 import android.app.Activity; 8 import android.app.Activity;
9 import android.content.ActivityNotFoundException; 9 import android.content.ActivityNotFoundException;
  10 +import android.content.ClipData;
10 import android.content.Intent; 11 import android.content.Intent;
11 import android.content.pm.PackageManager; 12 import android.content.pm.PackageManager;
12 import android.content.pm.ResolveInfo; 13 import android.content.pm.ResolveInfo;
@@ -294,10 +295,12 @@ public class ImagePickerDelegate @@ -294,10 +295,12 @@ public class ImagePickerDelegate
294 295
295 private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOptions) { 296 private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOptions) {
296 Intent pickMediaIntent; 297 Intent pickMediaIntent;
297 - if (generalOptions.getUsePhotoPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 298 + if (generalOptions.getUsePhotoPicker()) {
298 if (generalOptions.getAllowMultiple()) { 299 if (generalOptions.getAllowMultiple()) {
  300 + int limit = ImagePickerUtils.getLimitFromOption(generalOptions);
  301 +
299 pickMediaIntent = 302 pickMediaIntent =
300 - new ActivityResultContracts.PickMultipleVisualMedia() 303 + new ActivityResultContracts.PickMultipleVisualMedia(limit)
301 .createIntent( 304 .createIntent(
302 activity, 305 activity,
303 new PickVisualMediaRequest.Builder() 306 new PickVisualMediaRequest.Builder()
@@ -319,9 +322,7 @@ public class ImagePickerDelegate @@ -319,9 +322,7 @@ public class ImagePickerDelegate
319 pickMediaIntent.setType("*/*"); 322 pickMediaIntent.setType("*/*");
320 String[] mimeTypes = {"video/*", "image/*"}; 323 String[] mimeTypes = {"video/*", "image/*"};
321 pickMediaIntent.putExtra("CONTENT_TYPE", mimeTypes); 324 pickMediaIntent.putExtra("CONTENT_TYPE", mimeTypes);
322 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {  
323 - pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple());  
324 - } 325 + pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple());
325 } 326 }
326 activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY); 327 activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY);
327 } 328 }
@@ -340,7 +341,7 @@ public class ImagePickerDelegate @@ -340,7 +341,7 @@ public class ImagePickerDelegate
340 341
341 private void launchPickVideoFromGalleryIntent(Boolean usePhotoPicker) { 342 private void launchPickVideoFromGalleryIntent(Boolean usePhotoPicker) {
342 Intent pickVideoIntent; 343 Intent pickVideoIntent;
343 - if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 344 + if (usePhotoPicker) {
344 pickVideoIntent = 345 pickVideoIntent =
345 new ActivityResultContracts.PickVisualMedia() 346 new ActivityResultContracts.PickVisualMedia()
346 .createIntent( 347 .createIntent(
@@ -403,7 +404,7 @@ public class ImagePickerDelegate @@ -403,7 +404,7 @@ public class ImagePickerDelegate
403 } catch (ActivityNotFoundException e) { 404 } catch (ActivityNotFoundException e) {
404 try { 405 try {
405 // If we can't delete the file again here, there's not really anything we can do about it. 406 // If we can't delete the file again here, there's not really anything we can do about it.
406 - //noinspection ResultOfMethodCallIgnored 407 + // noinspection ResultOfMethodCallIgnored
407 videoFile.delete(); 408 videoFile.delete();
408 } catch (SecurityException exception) { 409 } catch (SecurityException exception) {
409 exception.printStackTrace(); 410 exception.printStackTrace();
@@ -427,18 +428,19 @@ public class ImagePickerDelegate @@ -427,18 +428,19 @@ public class ImagePickerDelegate
427 public void chooseMultiImageFromGallery( 428 public void chooseMultiImageFromGallery(
428 @NonNull ImageSelectionOptions options, 429 @NonNull ImageSelectionOptions options,
429 boolean usePhotoPicker, 430 boolean usePhotoPicker,
  431 + int limit,
430 @NonNull Messages.Result<List<String>> result) { 432 @NonNull Messages.Result<List<String>> result) {
431 if (!setPendingOptionsAndResult(options, null, result)) { 433 if (!setPendingOptionsAndResult(options, null, result)) {
432 finishWithAlreadyActiveError(result); 434 finishWithAlreadyActiveError(result);
433 return; 435 return;
434 } 436 }
435 437
436 - launchMultiPickImageFromGalleryIntent(usePhotoPicker); 438 + launchMultiPickImageFromGalleryIntent(usePhotoPicker, limit);
437 } 439 }
438 440
439 private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) { 441 private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) {
440 Intent pickImageIntent; 442 Intent pickImageIntent;
441 - if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 443 + if (usePhotoPicker) {
442 pickImageIntent = 444 pickImageIntent =
443 new ActivityResultContracts.PickVisualMedia() 445 new ActivityResultContracts.PickVisualMedia()
444 .createIntent( 446 .createIntent(
@@ -453,11 +455,11 @@ public class ImagePickerDelegate @@ -453,11 +455,11 @@ public class ImagePickerDelegate
453 activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY); 455 activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY);
454 } 456 }
455 457
456 - private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker) { 458 + private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker, int limit) {
457 Intent pickMultiImageIntent; 459 Intent pickMultiImageIntent;
458 - if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 460 + if (usePhotoPicker) {
459 pickMultiImageIntent = 461 pickMultiImageIntent =
460 - new ActivityResultContracts.PickMultipleVisualMedia() 462 + new ActivityResultContracts.PickMultipleVisualMedia(limit)
461 .createIntent( 463 .createIntent(
462 activity, 464 activity,
463 new PickVisualMediaRequest.Builder() 465 new PickVisualMediaRequest.Builder()
@@ -466,9 +468,7 @@ public class ImagePickerDelegate @@ -466,9 +468,7 @@ public class ImagePickerDelegate
466 } else { 468 } else {
467 pickMultiImageIntent = new Intent(Intent.ACTION_GET_CONTENT); 469 pickMultiImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
468 pickMultiImageIntent.setType("image/*"); 470 pickMultiImageIntent.setType("image/*");
469 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {  
470 - pickMultiImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);  
471 - } 471 + pickMultiImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
472 } 472 }
473 activity.startActivityForResult( 473 activity.startActivityForResult(
474 pickMultiImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY); 474 pickMultiImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY);
@@ -515,7 +515,7 @@ public class ImagePickerDelegate @@ -515,7 +515,7 @@ public class ImagePickerDelegate
515 } catch (ActivityNotFoundException e) { 515 } catch (ActivityNotFoundException e) {
516 try { 516 try {
517 // If we can't delete the file again here, there's not really anything we can do about it. 517 // If we can't delete the file again here, there's not really anything we can do about it.
518 - //noinspection ResultOfMethodCallIgnored 518 + // noinspection ResultOfMethodCallIgnored
519 imageFile.delete(); 519 imageFile.delete();
520 } catch (SecurityException exception) { 520 } catch (SecurityException exception) {
521 exception.printStackTrace(); 521 exception.printStackTrace();
@@ -549,10 +549,14 @@ public class ImagePickerDelegate @@ -549,10 +549,14 @@ public class ImagePickerDelegate
549 549
550 private void grantUriPermissions(Intent intent, Uri imageUri) { 550 private void grantUriPermissions(Intent intent, Uri imageUri) {
551 PackageManager packageManager = activity.getPackageManager(); 551 PackageManager packageManager = activity.getPackageManager();
552 - // TODO(stuartmorgan): Add new codepath: https://github.com/flutter/flutter/issues/121816  
553 - @SuppressWarnings("deprecation")  
554 - List<ResolveInfo> compatibleActivities =  
555 - packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 552 + List<ResolveInfo> compatibleActivities;
  553 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
  554 + compatibleActivities =
  555 + packageManager.queryIntentActivities(
  556 + intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
  557 + } else {
  558 + compatibleActivities = queryIntentActivitiesPreApi33(packageManager, intent);
  559 + }
556 560
557 for (ResolveInfo info : compatibleActivities) { 561 for (ResolveInfo info : compatibleActivities) {
558 activity.grantUriPermission( 562 activity.grantUriPermission(
@@ -562,6 +566,12 @@ public class ImagePickerDelegate @@ -562,6 +566,12 @@ public class ImagePickerDelegate
562 } 566 }
563 } 567 }
564 568
  569 + @SuppressWarnings("deprecation")
  570 + private static List<ResolveInfo> queryIntentActivitiesPreApi33(
  571 + PackageManager packageManager, Intent intent) {
  572 + return packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
  573 + }
  574 +
565 @Override 575 @Override
566 public boolean onRequestPermissionsResult( 576 public boolean onRequestPermissionsResult(
567 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 577 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
@@ -628,10 +638,57 @@ public class ImagePickerDelegate @@ -628,10 +638,57 @@ public class ImagePickerDelegate
628 return true; 638 return true;
629 } 639 }
630 640
  641 + @Nullable
  642 + private ArrayList<MediaPath> getPathsFromIntent(@NonNull Intent data, boolean includeMimeType) {
  643 + ArrayList<MediaPath> paths = new ArrayList<>();
  644 +
  645 + Uri uri = data.getData();
  646 + // On several pre-Android 13 devices using Android Photo Picker, the Uri from getData() could
  647 + // be null.
  648 + if (uri == null) {
  649 + ClipData clipData = data.getClipData();
  650 +
  651 + // If data.getData() and data.getClipData() are both null, we are in an error state. By
  652 + // convention we return null from here, and then finish with an error from the corresponding
  653 + // handler.
  654 + if (clipData == null) {
  655 + return null;
  656 + }
  657 +
  658 + for (int i = 0; i < data.getClipData().getItemCount(); i++) {
  659 + uri = data.getClipData().getItemAt(i).getUri();
  660 + // Same error state as above.
  661 + if (uri == null) {
  662 + return null;
  663 + }
  664 + String path = fileUtils.getPathFromUri(activity, uri);
  665 + // Again, same error state as above.
  666 + if (path == null) {
  667 + return null;
  668 + }
  669 + String mimeType = includeMimeType ? activity.getContentResolver().getType(uri) : null;
  670 + paths.add(new MediaPath(path, mimeType));
  671 + }
  672 + } else {
  673 + String path = fileUtils.getPathFromUri(activity, uri);
  674 + if (path == null) {
  675 + return null;
  676 + }
  677 + paths.add(new MediaPath(path, null));
  678 + }
  679 + return paths;
  680 + }
  681 +
631 private void handleChooseImageResult(int resultCode, Intent data) { 682 private void handleChooseImageResult(int resultCode, Intent data) {
632 if (resultCode == Activity.RESULT_OK && data != null) { 683 if (resultCode == Activity.RESULT_OK && data != null) {
633 - String path = fileUtils.getPathFromUri(activity, data.getData());  
634 - handleImageResult(path, false); 684 + ArrayList<MediaPath> paths = getPathsFromIntent(data, false);
  685 + // If there's no valid Uri, return an error
  686 + if (paths == null) {
  687 + finishWithError("no_valid_image_uri", "Cannot find the selected image.");
  688 + return;
  689 + }
  690 +
  691 + handleMediaResult(paths);
635 return; 692 return;
636 } 693 }
637 694
@@ -659,17 +716,13 @@ public class ImagePickerDelegate @@ -659,17 +716,13 @@ public class ImagePickerDelegate
659 716
660 private void handleChooseMediaResult(int resultCode, Intent intent) { 717 private void handleChooseMediaResult(int resultCode, Intent intent) {
661 if (resultCode == Activity.RESULT_OK && intent != null) { 718 if (resultCode == Activity.RESULT_OK && intent != null) {
662 - ArrayList<MediaPath> paths = new ArrayList<>();  
663 - if (intent.getClipData() != null) {  
664 - for (int i = 0; i < intent.getClipData().getItemCount(); i++) {  
665 - Uri uri = intent.getClipData().getItemAt(i).getUri();  
666 - String path = fileUtils.getPathFromUri(activity, uri);  
667 - String mimeType = activity.getContentResolver().getType(uri);  
668 - paths.add(new MediaPath(path, mimeType));  
669 - }  
670 - } else {  
671 - paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null)); 719 + ArrayList<MediaPath> paths = getPathsFromIntent(intent, true);
  720 + // If there's no valid Uri, return an error
  721 + if (paths == null) {
  722 + finishWithError("no_valid_media_uri", "Cannot find the selected media.");
  723 + return;
672 } 724 }
  725 +
673 handleMediaResult(paths); 726 handleMediaResult(paths);
674 return; 727 return;
675 } 728 }
@@ -680,17 +733,14 @@ public class ImagePickerDelegate @@ -680,17 +733,14 @@ public class ImagePickerDelegate
680 733
681 private void handleChooseMultiImageResult(int resultCode, Intent intent) { 734 private void handleChooseMultiImageResult(int resultCode, Intent intent) {
682 if (resultCode == Activity.RESULT_OK && intent != null) { 735 if (resultCode == Activity.RESULT_OK && intent != null) {
683 - ArrayList<MediaPath> paths = new ArrayList<>();  
684 - if (intent.getClipData() != null) {  
685 - for (int i = 0; i < intent.getClipData().getItemCount(); i++) {  
686 - paths.add(  
687 - new MediaPath(  
688 - fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()),  
689 - null));  
690 - }  
691 - } else {  
692 - paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null)); 736 + ArrayList<MediaPath> paths = getPathsFromIntent(intent, false);
  737 + // If there's no valid Uri, return an error
  738 + if (paths == null) {
  739 + finishWithError(
  740 + "missing_valid_image_uri", "Cannot find at least one of the selected images.");
  741 + return;
693 } 742 }
  743 +
694 handleMediaResult(paths); 744 handleMediaResult(paths);
695 return; 745 return;
696 } 746 }
@@ -701,8 +751,14 @@ public class ImagePickerDelegate @@ -701,8 +751,14 @@ public class ImagePickerDelegate
701 751
702 private void handleChooseVideoResult(int resultCode, Intent data) { 752 private void handleChooseVideoResult(int resultCode, Intent data) {
703 if (resultCode == Activity.RESULT_OK && data != null) { 753 if (resultCode == Activity.RESULT_OK && data != null) {
704 - String path = fileUtils.getPathFromUri(activity, data.getData());  
705 - handleVideoResult(path); 754 + ArrayList<MediaPath> paths = getPathsFromIntent(data, false);
  755 + // If there's no valid Uri, return an error
  756 + if (paths == null || paths.size() < 1) {
  757 + finishWithError("no_valid_video_uri", "Cannot find the selected video.");
  758 + return;
  759 + }
  760 +
  761 + finishWithSuccess(paths.get(0).path);
706 return; 762 return;
707 } 763 }
708 764
@@ -733,7 +789,7 @@ public class ImagePickerDelegate @@ -733,7 +789,7 @@ public class ImagePickerDelegate
733 localPendingCameraMediaUrl != null 789 localPendingCameraMediaUrl != null
734 ? localPendingCameraMediaUrl 790 ? localPendingCameraMediaUrl
735 : Uri.parse(cache.retrievePendingCameraMediaUriPath()), 791 : Uri.parse(cache.retrievePendingCameraMediaUriPath()),
736 - this::handleVideoResult); 792 + this::finishWithSuccess);
737 return; 793 return;
738 } 794 }
739 795
@@ -796,10 +852,6 @@ public class ImagePickerDelegate @@ -796,10 +852,6 @@ public class ImagePickerDelegate
796 } 852 }
797 } 853 }
798 854
799 - private void handleVideoResult(String path) {  
800 - finishWithSuccess(path);  
801 - }  
802 -  
803 private boolean setPendingOptionsAndResult( 855 private boolean setPendingOptionsAndResult(
804 @Nullable ImageSelectionOptions imageOptions, 856 @Nullable ImageSelectionOptions imageOptions,
805 @Nullable VideoSelectionOptions videoOptions, 857 @Nullable VideoSelectionOptions videoOptions,
@@ -18,7 +18,6 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -18,7 +18,6 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware;
18 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; 18 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
19 import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; 19 import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
20 import io.flutter.plugin.common.BinaryMessenger; 20 import io.flutter.plugin.common.BinaryMessenger;
21 -import io.flutter.plugin.common.PluginRegistry;  
22 import io.flutter.plugins.imagepicker.Messages.CacheRetrievalResult; 21 import io.flutter.plugins.imagepicker.Messages.CacheRetrievalResult;
23 import io.flutter.plugins.imagepicker.Messages.FlutterError; 22 import io.flutter.plugins.imagepicker.Messages.FlutterError;
24 import io.flutter.plugins.imagepicker.Messages.GeneralOptions; 23 import io.flutter.plugins.imagepicker.Messages.GeneralOptions;
@@ -117,7 +116,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @@ -117,7 +116,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
117 final Activity activity, 116 final Activity activity,
118 final BinaryMessenger messenger, 117 final BinaryMessenger messenger,
119 final ImagePickerApi handler, 118 final ImagePickerApi handler,
120 - final PluginRegistry.Registrar registrar,  
121 final ActivityPluginBinding activityBinding) { 119 final ActivityPluginBinding activityBinding) {
122 this.application = application; 120 this.application = application;
123 this.activity = activity; 121 this.activity = activity;
@@ -125,20 +123,14 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @@ -125,20 +123,14 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
125 this.messenger = messenger; 123 this.messenger = messenger;
126 124
127 delegate = constructDelegate(activity); 125 delegate = constructDelegate(activity);
128 - ImagePickerApi.setup(messenger, handler); 126 + ImagePickerApi.setUp(messenger, handler);
129 observer = new LifeCycleObserver(activity); 127 observer = new LifeCycleObserver(activity);
130 - if (registrar != null) {  
131 - // V1 embedding setup for activity listeners.  
132 - application.registerActivityLifecycleCallbacks(observer);  
133 - registrar.addActivityResultListener(delegate);  
134 - registrar.addRequestPermissionsResultListener(delegate);  
135 - } else {  
136 - // V2 embedding setup for activity listeners.  
137 - activityBinding.addActivityResultListener(delegate);  
138 - activityBinding.addRequestPermissionsResultListener(delegate);  
139 - lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);  
140 - lifecycle.addObserver(observer);  
141 - } 128 +
  129 + // V2 embedding setup for activity listeners.
  130 + activityBinding.addActivityResultListener(delegate);
  131 + activityBinding.addRequestPermissionsResultListener(delegate);
  132 + lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
  133 + lifecycle.addObserver(observer);
142 } 134 }
143 135
144 // Only invoked by {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing. 136 // Only invoked by {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing.
@@ -159,7 +151,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @@ -159,7 +151,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
159 lifecycle = null; 151 lifecycle = null;
160 } 152 }
161 153
162 - ImagePickerApi.setup(messenger, null); 154 + ImagePickerApi.setUp(messenger, null);
163 155
164 if (application != null) { 156 if (application != null) {
165 application.unregisterActivityLifecycleCallbacks(observer); 157 application.unregisterActivityLifecycleCallbacks(observer);
@@ -183,20 +175,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @@ -183,20 +175,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
183 private FlutterPluginBinding pluginBinding; 175 private FlutterPluginBinding pluginBinding;
184 ActivityState activityState; 176 ActivityState activityState;
185 177
186 - @SuppressWarnings("deprecation")  
187 - public static void registerWith(  
188 - @NonNull io.flutter.plugin.common.PluginRegistry.Registrar registrar) {  
189 - if (registrar.activity() == null) {  
190 - // If a background flutter view tries to register the plugin, there will be no activity from the registrar,  
191 - // we stop the registering process immediately because the ImagePicker requires an activity.  
192 - return;  
193 - }  
194 - Activity activity = registrar.activity();  
195 - Application application = (Application) (registrar.context().getApplicationContext());  
196 - ImagePickerPlugin plugin = new ImagePickerPlugin();  
197 - plugin.setup(registrar.messenger(), application, activity, registrar, null);  
198 - }  
199 -  
200 /** 178 /**
201 * Default constructor for the plugin. 179 * Default constructor for the plugin.
202 * 180 *
@@ -231,7 +209,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @@ -231,7 +209,6 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
231 pluginBinding.getBinaryMessenger(), 209 pluginBinding.getBinaryMessenger(),
232 (Application) pluginBinding.getApplicationContext(), 210 (Application) pluginBinding.getApplicationContext(),
233 binding.getActivity(), 211 binding.getActivity(),
234 - null,  
235 binding); 212 binding);
236 } 213 }
237 214
@@ -254,10 +231,8 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @@ -254,10 +231,8 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
254 final BinaryMessenger messenger, 231 final BinaryMessenger messenger,
255 final Application application, 232 final Application application,
256 final Activity activity, 233 final Activity activity,
257 - final PluginRegistry.Registrar registrar,  
258 final ActivityPluginBinding activityBinding) { 234 final ActivityPluginBinding activityBinding) {
259 - activityState =  
260 - new ActivityState(application, activity, messenger, this, registrar, activityBinding); 235 + activityState = new ActivityState(application, activity, messenger, this, activityBinding);
261 } 236 }
262 237
263 private void tearDown() { 238 private void tearDown() {
@@ -317,7 +292,10 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @@ -317,7 +292,10 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic
317 292
318 setCameraDevice(delegate, source); 293 setCameraDevice(delegate, source);
319 if (generalOptions.getAllowMultiple()) { 294 if (generalOptions.getAllowMultiple()) {
320 - delegate.chooseMultiImageFromGallery(options, generalOptions.getUsePhotoPicker(), result); 295 + int limit = ImagePickerUtils.getLimitFromOption(generalOptions);
  296 +
  297 + delegate.chooseMultiImageFromGallery(
  298 + options, generalOptions.getUsePhotoPicker(), limit, result);
321 } else { 299 } else {
322 switch (source.getType()) { 300 switch (source.getType()) {
323 case GALLERY: 301 case GALLERY:
@@ -5,10 +5,13 @@ @@ -5,10 +5,13 @@
5 package io.flutter.plugins.imagepicker; 5 package io.flutter.plugins.imagepicker;
6 6
7 import android.Manifest; 7 import android.Manifest;
  8 +import android.annotation.SuppressLint;
8 import android.content.Context; 9 import android.content.Context;
9 import android.content.pm.PackageInfo; 10 import android.content.pm.PackageInfo;
10 import android.content.pm.PackageManager; 11 import android.content.pm.PackageManager;
11 import android.os.Build; 12 import android.os.Build;
  13 +import android.provider.MediaStore;
  14 +import androidx.activity.result.contract.ActivityResultContracts;
12 import java.util.Arrays; 15 import java.util.Arrays;
13 16
14 final class ImagePickerUtils { 17 final class ImagePickerUtils {
@@ -16,10 +19,15 @@ final class ImagePickerUtils { @@ -16,10 +19,15 @@ final class ImagePickerUtils {
16 private static boolean isPermissionPresentInManifest(Context context, String permissionName) { 19 private static boolean isPermissionPresentInManifest(Context context, String permissionName) {
17 try { 20 try {
18 PackageManager packageManager = context.getPackageManager(); 21 PackageManager packageManager = context.getPackageManager();
19 - // TODO(stuartmorgan): Add new codepath: https://github.com/flutter/flutter/issues/121816  
20 - @SuppressWarnings("deprecation")  
21 - PackageInfo packageInfo =  
22 - packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); 22 + PackageInfo packageInfo;
  23 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
  24 + packageInfo =
  25 + packageManager.getPackageInfo(
  26 + context.getPackageName(),
  27 + PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
  28 + } else {
  29 + packageInfo = getPermissionsPackageInfoPreApi33(packageManager, context.getPackageName());
  30 + }
23 31
24 String[] requestedPermissions = packageInfo.requestedPermissions; 32 String[] requestedPermissions = packageInfo.requestedPermissions;
25 return Arrays.asList(requestedPermissions).contains(permissionName); 33 return Arrays.asList(requestedPermissions).contains(permissionName);
@@ -29,6 +37,13 @@ final class ImagePickerUtils { @@ -29,6 +37,13 @@ final class ImagePickerUtils {
29 } 37 }
30 } 38 }
31 39
  40 + @SuppressWarnings("deprecation")
  41 + private static PackageInfo getPermissionsPackageInfoPreApi33(
  42 + PackageManager packageManager, String packageName)
  43 + throws PackageManager.NameNotFoundException {
  44 + return packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
  45 + }
  46 +
32 /** 47 /**
33 * Camera permission need request if it present in manifest, because for M or great for take Photo 48 * Camera permission need request if it present in manifest, because for M or great for take Photo
34 * ar Video by intent need it permission, even if the camera permission is not used. 49 * ar Video by intent need it permission, even if the camera permission is not used.
@@ -42,4 +57,32 @@ final class ImagePickerUtils { @@ -42,4 +57,32 @@ final class ImagePickerUtils {
42 boolean greatOrEqualM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; 57 boolean greatOrEqualM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
43 return greatOrEqualM && isPermissionPresentInManifest(context, Manifest.permission.CAMERA); 58 return greatOrEqualM && isPermissionPresentInManifest(context, Manifest.permission.CAMERA);
44 } 59 }
  60 +
  61 + /**
  62 + * The system photo picker has a maximum limit of selectable items returned by
  63 + * [MediaStore.getPickImagesMaxLimit()] On devices supporting picker provided via
  64 + * [ACTION_SYSTEM_FALLBACK_PICK_IMAGES], the limit may be ignored if it's higher than the allowed
  65 + * limit. On devices not supporting the photo picker, the limit is ignored.
  66 + *
  67 + * @see MediaStore.EXTRA_PICK_IMAGES_MAX
  68 + */
  69 + @SuppressLint({"NewApi", "ClassVerificationFailure"})
  70 + static int getMaxItems() {
  71 + if (ActivityResultContracts.PickVisualMedia.isSystemPickerAvailable$activity_release()) {
  72 + return MediaStore.getPickImagesMaxLimit();
  73 + } else {
  74 + return Integer.MAX_VALUE;
  75 + }
  76 + }
  77 +
  78 + static int getLimitFromOption(Messages.GeneralOptions generalOptions) {
  79 + Long limit = generalOptions.getLimit();
  80 + int effectiveLimit = getMaxItems();
  81 +
  82 + if (limit != null && limit < effectiveLimit) {
  83 + effectiveLimit = Math.toIntExact(limit);
  84 + }
  85 +
  86 + return effectiveLimit;
  87 + }
45 } 88 }
@@ -10,7 +10,9 @@ import android.graphics.BitmapFactory; @@ -10,7 +10,9 @@ import android.graphics.BitmapFactory;
10 import android.util.Log; 10 import android.util.Log;
11 import androidx.annotation.NonNull; 11 import androidx.annotation.NonNull;
12 import androidx.annotation.Nullable; 12 import androidx.annotation.Nullable;
  13 +import androidx.annotation.VisibleForTesting;
13 import androidx.core.util.SizeFCompat; 14 import androidx.core.util.SizeFCompat;
  15 +import androidx.exifinterface.media.ExifInterface;
14 import java.io.ByteArrayOutputStream; 16 import java.io.ByteArrayOutputStream;
15 import java.io.File; 17 import java.io.File;
16 import java.io.FileOutputStream; 18 import java.io.FileOutputStream;
@@ -81,47 +83,34 @@ class ImageResizer { @@ -81,47 +83,34 @@ class ImageResizer {
81 } 83 }
82 84
83 private SizeFCompat calculateTargetSize( 85 private SizeFCompat calculateTargetSize(
84 - @NonNull Double originalWidth,  
85 - @NonNull Double originalHeight, 86 + double originalWidth,
  87 + double originalHeight,
86 @Nullable Double maxWidth, 88 @Nullable Double maxWidth,
87 @Nullable Double maxHeight) { 89 @Nullable Double maxHeight) {
  90 + double aspectRatio = originalWidth / originalHeight;
88 91
89 boolean hasMaxWidth = maxWidth != null; 92 boolean hasMaxWidth = maxWidth != null;
90 boolean hasMaxHeight = maxHeight != null; 93 boolean hasMaxHeight = maxHeight != null;
91 94
92 - Double width = hasMaxWidth ? Math.min(originalWidth, maxWidth) : originalWidth;  
93 - Double height = hasMaxHeight ? Math.min(originalHeight, maxHeight) : originalHeight; 95 + double width = hasMaxWidth ? Math.min(originalWidth, Math.round(maxWidth)) : originalWidth;
  96 + double height = hasMaxHeight ? Math.min(originalHeight, Math.round(maxHeight)) : originalHeight;
94 97
95 boolean shouldDownscaleWidth = hasMaxWidth && maxWidth < originalWidth; 98 boolean shouldDownscaleWidth = hasMaxWidth && maxWidth < originalWidth;
96 boolean shouldDownscaleHeight = hasMaxHeight && maxHeight < originalHeight; 99 boolean shouldDownscaleHeight = hasMaxHeight && maxHeight < originalHeight;
97 boolean shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight; 100 boolean shouldDownscale = shouldDownscaleWidth || shouldDownscaleHeight;
98 101
99 if (shouldDownscale) { 102 if (shouldDownscale) {
100 - double downscaledWidth = (height / originalHeight) * originalWidth;  
101 - double downscaledHeight = (width / originalWidth) * originalHeight;  
102 -  
103 - if (width < height) {  
104 - if (!hasMaxWidth) {  
105 - width = downscaledWidth;  
106 - } else {  
107 - height = downscaledHeight;  
108 - }  
109 - } else if (height < width) {  
110 - if (!hasMaxHeight) {  
111 - height = downscaledHeight;  
112 - } else {  
113 - width = downscaledWidth;  
114 - } 103 + double WidthForMaxHeight = height * aspectRatio;
  104 + double heightForMaxWidth = width / aspectRatio;
  105 +
  106 + if (heightForMaxWidth > height) {
  107 + width = (double) Math.round(WidthForMaxHeight);
115 } else { 108 } else {
116 - if (originalWidth < originalHeight) {  
117 - width = downscaledWidth;  
118 - } else if (originalHeight < originalWidth) {  
119 - height = downscaledHeight;  
120 - } 109 + height = (double) Math.round(heightForMaxWidth);
121 } 110 }
122 } 111 }
123 112
124 - return new SizeFCompat(width.floatValue(), height.floatValue()); 113 + return new SizeFCompat((float) width, (float) height);
125 } 114 }
126 115
127 private File createFile(File externalFilesDirectory, String child) { 116 private File createFile(File externalFilesDirectory, String child) {
@@ -137,10 +126,15 @@ class ImageResizer { @@ -137,10 +126,15 @@ class ImageResizer {
137 } 126 }
138 127
139 private void copyExif(String filePathOri, String filePathDest) { 128 private void copyExif(String filePathOri, String filePathDest) {
140 - exifDataCopier.copyExif(filePathOri, filePathDest); 129 + try {
  130 + exifDataCopier.copyExif(new ExifInterface(filePathOri), new ExifInterface(filePathDest));
  131 + } catch (Exception ex) {
  132 + Log.e("ImageResizer", "Error preserving Exif data on selected image: " + ex);
  133 + }
141 } 134 }
142 135
143 - private SizeFCompat readFileDimensions(String path) { 136 + @VisibleForTesting
  137 + SizeFCompat readFileDimensions(String path) {
144 BitmapFactory.Options options = new BitmapFactory.Options(); 138 BitmapFactory.Options options = new BitmapFactory.Options();
145 options.inJustDecodeBounds = true; 139 options.inJustDecodeBounds = true;
146 decodeFile(path, options); 140 decodeFile(path, options);
@@ -6,6 +6,9 @@ @@ -6,6 +6,9 @@
6 6
7 package io.flutter.plugins.imagepicker; 7 package io.flutter.plugins.imagepicker;
8 8
  9 +import static java.lang.annotation.ElementType.METHOD;
  10 +import static java.lang.annotation.RetentionPolicy.CLASS;
  11 +
9 import android.util.Log; 12 import android.util.Log;
10 import androidx.annotation.NonNull; 13 import androidx.annotation.NonNull;
11 import androidx.annotation.Nullable; 14 import androidx.annotation.Nullable;
@@ -14,6 +17,8 @@ import io.flutter.plugin.common.BinaryMessenger; @@ -14,6 +17,8 @@ import io.flutter.plugin.common.BinaryMessenger;
14 import io.flutter.plugin.common.MessageCodec; 17 import io.flutter.plugin.common.MessageCodec;
15 import io.flutter.plugin.common.StandardMessageCodec; 18 import io.flutter.plugin.common.StandardMessageCodec;
16 import java.io.ByteArrayOutputStream; 19 import java.io.ByteArrayOutputStream;
  20 +import java.lang.annotation.Retention;
  21 +import java.lang.annotation.Target;
17 import java.nio.ByteBuffer; 22 import java.nio.ByteBuffer;
18 import java.util.ArrayList; 23 import java.util.ArrayList;
19 import java.util.List; 24 import java.util.List;
@@ -55,6 +60,10 @@ public class Messages { @@ -55,6 +60,10 @@ public class Messages {
55 return errorList; 60 return errorList;
56 } 61 }
57 62
  63 + @Target(METHOD)
  64 + @Retention(CLASS)
  65 + @interface CanIgnoreReturnValue {}
  66 +
58 public enum SourceCamera { 67 public enum SourceCamera {
59 REAR(0), 68 REAR(0),
60 FRONT(1); 69 FRONT(1);
@@ -116,6 +125,16 @@ public class Messages { @@ -116,6 +125,16 @@ public class Messages {
116 this.usePhotoPicker = setterArg; 125 this.usePhotoPicker = setterArg;
117 } 126 }
118 127
  128 + private @Nullable Long limit;
  129 +
  130 + public @Nullable Long getLimit() {
  131 + return limit;
  132 + }
  133 +
  134 + public void setLimit(@Nullable Long setterArg) {
  135 + this.limit = setterArg;
  136 + }
  137 +
119 /** Constructor is non-public to enforce null safety; use Builder. */ 138 /** Constructor is non-public to enforce null safety; use Builder. */
120 GeneralOptions() {} 139 GeneralOptions() {}
121 140
@@ -123,6 +142,7 @@ public class Messages { @@ -123,6 +142,7 @@ public class Messages {
123 142
124 private @Nullable Boolean allowMultiple; 143 private @Nullable Boolean allowMultiple;
125 144
  145 + @CanIgnoreReturnValue
126 public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) { 146 public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) {
127 this.allowMultiple = setterArg; 147 this.allowMultiple = setterArg;
128 return this; 148 return this;
@@ -130,24 +150,35 @@ public class Messages { @@ -130,24 +150,35 @@ public class Messages {
130 150
131 private @Nullable Boolean usePhotoPicker; 151 private @Nullable Boolean usePhotoPicker;
132 152
  153 + @CanIgnoreReturnValue
133 public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) { 154 public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) {
134 this.usePhotoPicker = setterArg; 155 this.usePhotoPicker = setterArg;
135 return this; 156 return this;
136 } 157 }
137 158
  159 + private @Nullable Long limit;
  160 +
  161 + @CanIgnoreReturnValue
  162 + public @NonNull Builder setLimit(@Nullable Long setterArg) {
  163 + this.limit = setterArg;
  164 + return this;
  165 + }
  166 +
138 public @NonNull GeneralOptions build() { 167 public @NonNull GeneralOptions build() {
139 GeneralOptions pigeonReturn = new GeneralOptions(); 168 GeneralOptions pigeonReturn = new GeneralOptions();
140 pigeonReturn.setAllowMultiple(allowMultiple); 169 pigeonReturn.setAllowMultiple(allowMultiple);
141 pigeonReturn.setUsePhotoPicker(usePhotoPicker); 170 pigeonReturn.setUsePhotoPicker(usePhotoPicker);
  171 + pigeonReturn.setLimit(limit);
142 return pigeonReturn; 172 return pigeonReturn;
143 } 173 }
144 } 174 }
145 175
146 @NonNull 176 @NonNull
147 ArrayList<Object> toList() { 177 ArrayList<Object> toList() {
148 - ArrayList<Object> toListResult = new ArrayList<Object>(2); 178 + ArrayList<Object> toListResult = new ArrayList<Object>(3);
149 toListResult.add(allowMultiple); 179 toListResult.add(allowMultiple);
150 toListResult.add(usePhotoPicker); 180 toListResult.add(usePhotoPicker);
  181 + toListResult.add(limit);
151 return toListResult; 182 return toListResult;
152 } 183 }
153 184
@@ -157,6 +188,9 @@ public class Messages { @@ -157,6 +188,9 @@ public class Messages {
157 pigeonResult.setAllowMultiple((Boolean) allowMultiple); 188 pigeonResult.setAllowMultiple((Boolean) allowMultiple);
158 Object usePhotoPicker = list.get(1); 189 Object usePhotoPicker = list.get(1);
159 pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker); 190 pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker);
  191 + Object limit = list.get(2);
  192 + pigeonResult.setLimit(
  193 + (limit == null) ? null : ((limit instanceof Integer) ? (Integer) limit : (Long) limit));
160 return pigeonResult; 194 return pigeonResult;
161 } 195 }
162 } 196 }
@@ -214,6 +248,7 @@ public class Messages { @@ -214,6 +248,7 @@ public class Messages {
214 248
215 private @Nullable Double maxWidth; 249 private @Nullable Double maxWidth;
216 250
  251 + @CanIgnoreReturnValue
217 public @NonNull Builder setMaxWidth(@Nullable Double setterArg) { 252 public @NonNull Builder setMaxWidth(@Nullable Double setterArg) {
218 this.maxWidth = setterArg; 253 this.maxWidth = setterArg;
219 return this; 254 return this;
@@ -221,6 +256,7 @@ public class Messages { @@ -221,6 +256,7 @@ public class Messages {
221 256
222 private @Nullable Double maxHeight; 257 private @Nullable Double maxHeight;
223 258
  259 + @CanIgnoreReturnValue
224 public @NonNull Builder setMaxHeight(@Nullable Double setterArg) { 260 public @NonNull Builder setMaxHeight(@Nullable Double setterArg) {
225 this.maxHeight = setterArg; 261 this.maxHeight = setterArg;
226 return this; 262 return this;
@@ -228,6 +264,7 @@ public class Messages { @@ -228,6 +264,7 @@ public class Messages {
228 264
229 private @Nullable Long quality; 265 private @Nullable Long quality;
230 266
  267 + @CanIgnoreReturnValue
231 public @NonNull Builder setQuality(@NonNull Long setterArg) { 268 public @NonNull Builder setQuality(@NonNull Long setterArg) {
232 this.quality = setterArg; 269 this.quality = setterArg;
233 return this; 270 return this;
@@ -288,6 +325,7 @@ public class Messages { @@ -288,6 +325,7 @@ public class Messages {
288 325
289 private @Nullable ImageSelectionOptions imageSelectionOptions; 326 private @Nullable ImageSelectionOptions imageSelectionOptions;
290 327
  328 + @CanIgnoreReturnValue
291 public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) { 329 public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) {
292 this.imageSelectionOptions = setterArg; 330 this.imageSelectionOptions = setterArg;
293 return this; 331 return this;
@@ -339,6 +377,7 @@ public class Messages { @@ -339,6 +377,7 @@ public class Messages {
339 377
340 private @Nullable Long maxDurationSeconds; 378 private @Nullable Long maxDurationSeconds;
341 379
  380 + @CanIgnoreReturnValue
342 public @NonNull Builder setMaxDurationSeconds(@Nullable Long setterArg) { 381 public @NonNull Builder setMaxDurationSeconds(@Nullable Long setterArg) {
343 this.maxDurationSeconds = setterArg; 382 this.maxDurationSeconds = setterArg;
344 return this; 383 return this;
@@ -407,6 +446,7 @@ public class Messages { @@ -407,6 +446,7 @@ public class Messages {
407 446
408 private @Nullable SourceType type; 447 private @Nullable SourceType type;
409 448
  449 + @CanIgnoreReturnValue
410 public @NonNull Builder setType(@NonNull SourceType setterArg) { 450 public @NonNull Builder setType(@NonNull SourceType setterArg) {
411 this.type = setterArg; 451 this.type = setterArg;
412 return this; 452 return this;
@@ -414,6 +454,7 @@ public class Messages { @@ -414,6 +454,7 @@ public class Messages {
414 454
415 private @Nullable SourceCamera camera; 455 private @Nullable SourceCamera camera;
416 456
  457 + @CanIgnoreReturnValue
417 public @NonNull Builder setCamera(@Nullable SourceCamera setterArg) { 458 public @NonNull Builder setCamera(@Nullable SourceCamera setterArg) {
418 this.camera = setterArg; 459 this.camera = setterArg;
419 return this; 460 return this;
@@ -438,7 +479,7 @@ public class Messages { @@ -438,7 +479,7 @@ public class Messages {
438 static @NonNull SourceSpecification fromList(@NonNull ArrayList<Object> list) { 479 static @NonNull SourceSpecification fromList(@NonNull ArrayList<Object> list) {
439 SourceSpecification pigeonResult = new SourceSpecification(); 480 SourceSpecification pigeonResult = new SourceSpecification();
440 Object type = list.get(0); 481 Object type = list.get(0);
441 - pigeonResult.setType(type == null ? null : SourceType.values()[(int) type]); 482 + pigeonResult.setType(SourceType.values()[(int) type]);
442 Object camera = list.get(1); 483 Object camera = list.get(1);
443 pigeonResult.setCamera(camera == null ? null : SourceCamera.values()[(int) camera]); 484 pigeonResult.setCamera(camera == null ? null : SourceCamera.values()[(int) camera]);
444 return pigeonResult; 485 return pigeonResult;
@@ -483,6 +524,7 @@ public class Messages { @@ -483,6 +524,7 @@ public class Messages {
483 524
484 private @Nullable String code; 525 private @Nullable String code;
485 526
  527 + @CanIgnoreReturnValue
486 public @NonNull Builder setCode(@NonNull String setterArg) { 528 public @NonNull Builder setCode(@NonNull String setterArg) {
487 this.code = setterArg; 529 this.code = setterArg;
488 return this; 530 return this;
@@ -490,6 +532,7 @@ public class Messages { @@ -490,6 +532,7 @@ public class Messages {
490 532
491 private @Nullable String message; 533 private @Nullable String message;
492 534
  535 + @CanIgnoreReturnValue
493 public @NonNull Builder setMessage(@Nullable String setterArg) { 536 public @NonNull Builder setMessage(@Nullable String setterArg) {
494 this.message = setterArg; 537 this.message = setterArg;
495 return this; 538 return this;
@@ -578,6 +621,7 @@ public class Messages { @@ -578,6 +621,7 @@ public class Messages {
578 621
579 private @Nullable CacheRetrievalType type; 622 private @Nullable CacheRetrievalType type;
580 623
  624 + @CanIgnoreReturnValue
581 public @NonNull Builder setType(@NonNull CacheRetrievalType setterArg) { 625 public @NonNull Builder setType(@NonNull CacheRetrievalType setterArg) {
582 this.type = setterArg; 626 this.type = setterArg;
583 return this; 627 return this;
@@ -585,6 +629,7 @@ public class Messages { @@ -585,6 +629,7 @@ public class Messages {
585 629
586 private @Nullable CacheRetrievalError error; 630 private @Nullable CacheRetrievalError error;
587 631
  632 + @CanIgnoreReturnValue
588 public @NonNull Builder setError(@Nullable CacheRetrievalError setterArg) { 633 public @NonNull Builder setError(@Nullable CacheRetrievalError setterArg) {
589 this.error = setterArg; 634 this.error = setterArg;
590 return this; 635 return this;
@@ -592,6 +637,7 @@ public class Messages { @@ -592,6 +637,7 @@ public class Messages {
592 637
593 private @Nullable List<String> paths; 638 private @Nullable List<String> paths;
594 639
  640 + @CanIgnoreReturnValue
595 public @NonNull Builder setPaths(@NonNull List<String> setterArg) { 641 public @NonNull Builder setPaths(@NonNull List<String> setterArg) {
596 this.paths = setterArg; 642 this.paths = setterArg;
597 return this; 643 return this;
@@ -618,7 +664,7 @@ public class Messages { @@ -618,7 +664,7 @@ public class Messages {
618 static @NonNull CacheRetrievalResult fromList(@NonNull ArrayList<Object> list) { 664 static @NonNull CacheRetrievalResult fromList(@NonNull ArrayList<Object> list) {
619 CacheRetrievalResult pigeonResult = new CacheRetrievalResult(); 665 CacheRetrievalResult pigeonResult = new CacheRetrievalResult();
620 Object type = list.get(0); 666 Object type = list.get(0);
621 - pigeonResult.setType(type == null ? null : CacheRetrievalType.values()[(int) type]); 667 + pigeonResult.setType(CacheRetrievalType.values()[(int) type]);
622 Object error = list.get(1); 668 Object error = list.get(1);
623 pigeonResult.setError( 669 pigeonResult.setError(
624 (error == null) ? null : CacheRetrievalError.fromList((ArrayList<Object>) error)); 670 (error == null) ? null : CacheRetrievalError.fromList((ArrayList<Object>) error));
@@ -734,7 +780,7 @@ public class Messages { @@ -734,7 +780,7 @@ public class Messages {
734 return ImagePickerApiCodec.INSTANCE; 780 return ImagePickerApiCodec.INSTANCE;
735 } 781 }
736 /** Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. */ 782 /** Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. */
737 - static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePickerApi api) { 783 + static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePickerApi api) {
738 { 784 {
739 BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); 785 BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue();
740 BasicMessageChannel<Object> channel = 786 BasicMessageChannel<Object> channel =
@@ -67,6 +67,7 @@ class ImagePickerAndroid extends ImagePickerPlatform { @@ -67,6 +67,7 @@ class ImagePickerAndroid extends ImagePickerPlatform {
67 double? maxWidth, 67 double? maxWidth,
68 double? maxHeight, 68 double? maxHeight,
69 int? imageQuality, 69 int? imageQuality,
  70 + int? limit,
70 }) { 71 }) {
71 if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { 72 if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
72 throw ArgumentError.value( 73 throw ArgumentError.value(
@@ -81,6 +82,10 @@ class ImagePickerAndroid extends ImagePickerPlatform { @@ -81,6 +82,10 @@ class ImagePickerAndroid extends ImagePickerPlatform {
81 throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); 82 throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
82 } 83 }
83 84
  85 + if (limit != null && limit < 2) {
  86 + throw ArgumentError.value(limit, 'limit', 'cannot be lower than 2');
  87 + }
  88 +
84 return _hostApi.pickImages( 89 return _hostApi.pickImages(
85 SourceSpecification(type: SourceType.gallery), 90 SourceSpecification(type: SourceType.gallery),
86 ImageSelectionOptions( 91 ImageSelectionOptions(
@@ -88,7 +93,10 @@ class ImagePickerAndroid extends ImagePickerPlatform { @@ -88,7 +93,10 @@ class ImagePickerAndroid extends ImagePickerPlatform {
88 maxHeight: maxHeight, 93 maxHeight: maxHeight,
89 quality: imageQuality ?? 100), 94 quality: imageQuality ?? 100),
90 GeneralOptions( 95 GeneralOptions(
91 - allowMultiple: true, usePhotoPicker: useAndroidPhotoPicker), 96 + allowMultiple: true,
  97 + usePhotoPicker: useAndroidPhotoPicker,
  98 + limit: limit,
  99 + ),
92 ); 100 );
93 } 101 }
94 102
@@ -210,15 +218,30 @@ class ImagePickerAndroid extends ImagePickerPlatform { @@ -210,15 +218,30 @@ class ImagePickerAndroid extends ImagePickerPlatform {
210 } 218 }
211 219
212 @override 220 @override
  221 + Future<List<XFile>> getMultiImageWithOptions({
  222 + MultiImagePickerOptions options = const MultiImagePickerOptions(),
  223 + }) async {
  224 + final List<dynamic> paths = await _getMultiImagePath(
  225 + maxWidth: options.imageOptions.maxWidth,
  226 + maxHeight: options.imageOptions.maxHeight,
  227 + imageQuality: options.imageOptions.imageQuality,
  228 + limit: options.limit,
  229 + );
  230 +
  231 + if (paths.isEmpty) {
  232 + return <XFile>[];
  233 + }
  234 +
  235 + return paths.map((dynamic path) => XFile(path as String)).toList();
  236 + }
  237 +
  238 + @override
213 Future<List<XFile>> getMedia({ 239 Future<List<XFile>> getMedia({
214 required MediaOptions options, 240 required MediaOptions options,
215 }) async { 241 }) async {
216 return (await _hostApi.pickMedia( 242 return (await _hostApi.pickMedia(
217 _mediaOptionsToMediaSelectionOptions(options), 243 _mediaOptionsToMediaSelectionOptions(options),
218 - GeneralOptions(  
219 - allowMultiple: options.allowMultiple,  
220 - usePhotoPicker: useAndroidPhotoPicker,  
221 - ), 244 + _mediaOptionsToGeneralOptions(options),
222 )) 245 ))
223 .map((String? path) => XFile(path!)) 246 .map((String? path) => XFile(path!))
224 .toList(); 247 .toList();
@@ -243,6 +266,7 @@ class ImagePickerAndroid extends ImagePickerPlatform { @@ -243,6 +266,7 @@ class ImagePickerAndroid extends ImagePickerPlatform {
243 final ImageSelectionOptions imageSelectionOptions = 266 final ImageSelectionOptions imageSelectionOptions =
244 _imageOptionsToImageSelectionOptionsWithValidator( 267 _imageOptionsToImageSelectionOptionsWithValidator(
245 mediaOptions.imageOptions); 268 mediaOptions.imageOptions);
  269 +
246 return MediaSelectionOptions( 270 return MediaSelectionOptions(
247 imageSelectionOptions: imageSelectionOptions, 271 imageSelectionOptions: imageSelectionOptions,
248 ); 272 );
@@ -270,6 +294,29 @@ class ImagePickerAndroid extends ImagePickerPlatform { @@ -270,6 +294,29 @@ class ImagePickerAndroid extends ImagePickerPlatform {
270 quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth); 294 quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth);
271 } 295 }
272 296
  297 + GeneralOptions _mediaOptionsToGeneralOptions(MediaOptions options) {
  298 + final bool allowMultiple = options.allowMultiple;
  299 + final int? limit = options.limit;
  300 +
  301 + if (!allowMultiple && limit != null) {
  302 + throw ArgumentError.value(
  303 + allowMultiple,
  304 + 'allowMultiple',
  305 + 'cannot be false, when limit is not null',
  306 + );
  307 + }
  308 +
  309 + if (limit != null && limit < 2) {
  310 + throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2');
  311 + }
  312 +
  313 + return GeneralOptions(
  314 + allowMultiple: allowMultiple,
  315 + usePhotoPicker: useAndroidPhotoPicker,
  316 + limit: limit,
  317 + );
  318 + }
  319 +
273 @override 320 @override
274 Future<LostData> retrieveLostData() async { 321 Future<LostData> retrieveLostData() async {
275 final LostDataResponse result = await getLostData(); 322 final LostDataResponse result = await getLostData();
@@ -30,16 +30,20 @@ class GeneralOptions { @@ -30,16 +30,20 @@ class GeneralOptions {
30 GeneralOptions({ 30 GeneralOptions({
31 required this.allowMultiple, 31 required this.allowMultiple,
32 required this.usePhotoPicker, 32 required this.usePhotoPicker,
  33 + this.limit,
33 }); 34 });
34 35
35 bool allowMultiple; 36 bool allowMultiple;
36 37
37 bool usePhotoPicker; 38 bool usePhotoPicker;
38 39
  40 + int? limit;
  41 +
39 Object encode() { 42 Object encode() {
40 return <Object?>[ 43 return <Object?>[
41 allowMultiple, 44 allowMultiple,
42 usePhotoPicker, 45 usePhotoPicker,
  46 + limit,
43 ]; 47 ];
44 } 48 }
45 49
@@ -48,6 +52,7 @@ class GeneralOptions { @@ -48,6 +52,7 @@ class GeneralOptions {
48 return GeneralOptions( 52 return GeneralOptions(
49 allowMultiple: result[0]! as bool, 53 allowMultiple: result[0]! as bool,
50 usePhotoPicker: result[1]! as bool, 54 usePhotoPicker: result[1]! as bool,
  55 + limit: result[2] as int?,
51 ); 56 );
52 } 57 }
53 } 58 }
@@ -195,7 +200,7 @@ class CacheRetrievalResult { @@ -195,7 +200,7 @@ class CacheRetrievalResult {
195 CacheRetrievalResult({ 200 CacheRetrievalResult({
196 required this.type, 201 required this.type,
197 this.error, 202 this.error,
198 - required this.paths, 203 + this.paths = const <String>[],
199 }); 204 });
200 205
201 /// The type of the retrieved data. 206 /// The type of the retrieved data.
@@ -56,6 +56,7 @@ class ImagePickerOhos extends ImagePickerPlatform { @@ -56,6 +56,7 @@ class ImagePickerOhos extends ImagePickerPlatform {
56 double? maxWidth, 56 double? maxWidth,
57 double? maxHeight, 57 double? maxHeight,
58 int? imageQuality, 58 int? imageQuality,
  59 + int? limit,
59 }) { 60 }) {
60 if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { 61 if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
61 throw ArgumentError.value( 62 throw ArgumentError.value(
@@ -70,6 +71,10 @@ class ImagePickerOhos extends ImagePickerPlatform { @@ -70,6 +71,10 @@ class ImagePickerOhos extends ImagePickerPlatform {
70 throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); 71 throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
71 } 72 }
72 73
  74 + if (limit != null && limit < 2) {
  75 + throw ArgumentError.value(limit, 'limit', 'cannot be lower than 2');
  76 + }
  77 +
73 return _hostApi.pickImages( 78 return _hostApi.pickImages(
74 SourceSpecification(type: SourceType.gallery), 79 SourceSpecification(type: SourceType.gallery),
75 ImageSelectionOptions( 80 ImageSelectionOptions(
@@ -77,7 +82,10 @@ class ImagePickerOhos extends ImagePickerPlatform { @@ -77,7 +82,10 @@ class ImagePickerOhos extends ImagePickerPlatform {
77 maxHeight: maxHeight, 82 maxHeight: maxHeight,
78 quality: imageQuality ?? 100), 83 quality: imageQuality ?? 100),
79 GeneralOptions( 84 GeneralOptions(
80 - allowMultiple: true, usePhotoPicker: useOhosPhotoPicker), 85 + allowMultiple: true,
  86 + usePhotoPicker: useOhosPhotoPicker,
  87 + limit: limit,
  88 + ),
81 ); 89 );
82 } 90 }
83 91
@@ -199,18 +207,33 @@ class ImagePickerOhos extends ImagePickerPlatform { @@ -199,18 +207,33 @@ class ImagePickerOhos extends ImagePickerPlatform {
199 } 207 }
200 208
201 @override 209 @override
  210 + Future<List<XFile>> getMultiImageWithOptions({
  211 + MultiImagePickerOptions options = const MultiImagePickerOptions(),
  212 + }) async {
  213 + final List<dynamic> paths = await _getMultiImagePath(
  214 + maxWidth: options.imageOptions.maxWidth,
  215 + maxHeight: options.imageOptions.maxHeight,
  216 + imageQuality: options.imageOptions.imageQuality,
  217 + limit: options.limit,
  218 + );
  219 +
  220 + if (paths.isEmpty) {
  221 + return <XFile>[];
  222 + }
  223 +
  224 + return paths.map((dynamic path) => XFile(path as String)).toList();
  225 + }
  226 +
  227 + @override
202 Future<List<XFile>> getMedia({ 228 Future<List<XFile>> getMedia({
203 required MediaOptions options, 229 required MediaOptions options,
204 }) async { 230 }) async {
205 return (await _hostApi.pickMedia( 231 return (await _hostApi.pickMedia(
206 _mediaOptionsToMediaSelectionOptions(options), 232 _mediaOptionsToMediaSelectionOptions(options),
207 - GeneralOptions(  
208 - allowMultiple: options.allowMultiple,  
209 - usePhotoPicker: useOhosPhotoPicker,  
210 - ),  
211 - ))  
212 - .map((String? path) => XFile(path!))  
213 - .toList(); 233 + _mediaOptionsToGeneralOptions(options),
  234 + ))
  235 + .map((String? path) => XFile(path!))
  236 + .toList();
214 } 237 }
215 238
216 @override 239 @override
@@ -232,6 +255,7 @@ class ImagePickerOhos extends ImagePickerPlatform { @@ -232,6 +255,7 @@ class ImagePickerOhos extends ImagePickerPlatform {
232 final ImageSelectionOptions imageSelectionOptions = 255 final ImageSelectionOptions imageSelectionOptions =
233 _imageOptionsToImageSelectionOptionsWithValidator( 256 _imageOptionsToImageSelectionOptionsWithValidator(
234 mediaOptions.imageOptions); 257 mediaOptions.imageOptions);
  258 +
235 return MediaSelectionOptions( 259 return MediaSelectionOptions(
236 imageSelectionOptions: imageSelectionOptions, 260 imageSelectionOptions: imageSelectionOptions,
237 ); 261 );
@@ -259,6 +283,29 @@ class ImagePickerOhos extends ImagePickerPlatform { @@ -259,6 +283,29 @@ class ImagePickerOhos extends ImagePickerPlatform {
259 quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth); 283 quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth);
260 } 284 }
261 285
  286 + GeneralOptions _mediaOptionsToGeneralOptions(MediaOptions options) {
  287 + final bool allowMultiple = options.allowMultiple;
  288 + final int? limit = options.limit;
  289 +
  290 + if (!allowMultiple && limit != null) {
  291 + throw ArgumentError.value(
  292 + allowMultiple,
  293 + 'allowMultiple',
  294 + 'cannot be false, when limit is not null',
  295 + );
  296 + }
  297 +
  298 + if (limit != null && limit < 2) {
  299 + throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2');
  300 + }
  301 +
  302 + return GeneralOptions(
  303 + allowMultiple: allowMultiple,
  304 + usePhotoPicker: useOhosPhotoPicker,
  305 + limit: limit,
  306 + );
  307 + }
  308 +
262 @override 309 @override
263 Future<LostData> retrieveLostData() async { 310 Future<LostData> retrieveLostData() async {
264 final LostDataResponse result = await getLostData(); 311 final LostDataResponse result = await getLostData();
@@ -30,16 +30,20 @@ class GeneralOptions { @@ -30,16 +30,20 @@ class GeneralOptions {
30 GeneralOptions({ 30 GeneralOptions({
31 required this.allowMultiple, 31 required this.allowMultiple,
32 required this.usePhotoPicker, 32 required this.usePhotoPicker,
  33 + this.limit,
33 }); 34 });
34 35
35 bool allowMultiple; 36 bool allowMultiple;
36 37
37 bool usePhotoPicker; 38 bool usePhotoPicker;
38 39
  40 + int? limit;
  41 +
39 Object encode() { 42 Object encode() {
40 return <Object?>[ 43 return <Object?>[
41 allowMultiple, 44 allowMultiple,
42 usePhotoPicker, 45 usePhotoPicker,
  46 + limit,
43 ]; 47 ];
44 } 48 }
45 49
@@ -48,6 +52,7 @@ class GeneralOptions { @@ -48,6 +52,7 @@ class GeneralOptions {
48 return GeneralOptions( 52 return GeneralOptions(
49 allowMultiple: result[0]! as bool, 53 allowMultiple: result[0]! as bool,
50 usePhotoPicker: result[1]! as bool, 54 usePhotoPicker: result[1]! as bool,
  55 + limit: result[2] as int?,
51 ); 56 );
52 } 57 }
53 } 58 }
@@ -195,7 +200,7 @@ class CacheRetrievalResult { @@ -195,7 +200,7 @@ class CacheRetrievalResult {
195 CacheRetrievalResult({ 200 CacheRetrievalResult({
196 required this.type, 201 required this.type,
197 this.error, 202 this.error,
198 - required this.paths, 203 + this.paths = const <String>[],
199 }); 204 });
200 205
201 /// The type of the retrieved data. 206 /// The type of the retrieved data.
@@ -163,7 +163,7 @@ export default class ImagePickerDelegate { @@ -163,7 +163,7 @@ export default class ImagePickerDelegate {
163 return; 163 return;
164 } 164 }
165 165
166 - this.chooseMedia(generalOptions.getAllowMultiple() ? 9 : 1, 'handleChooseMediaResult') 166 + this.chooseMedia(generalOptions.getAllowMultiple() ? generalOptions.getLimit() : 1, 'handleChooseMediaResult')
167 } 167 }
168 168
169 handleChooseMediaResult(code: number, uris: Array<string>): void { 169 handleChooseMediaResult(code: number, uris: Array<string>): void {
@@ -279,13 +279,13 @@ export default class ImagePickerDelegate { @@ -279,13 +279,13 @@ export default class ImagePickerDelegate {
279 } 279 }
280 280
281 // 选择多个图片 281 // 选择多个图片
282 - chooseMultiImagesFromGallery(options: ImageSelectionOptions, usePhotoPicker: boolean, result: Result<ArrayList<string>>): void { 282 + chooseMultiImagesFromGallery(options: ImageSelectionOptions,limit: number, usePhotoPicker: boolean, result: Result<ArrayList<string>>): void {
283 if (!this.setPendingOptionsAndResult(options, null, result)) { 283 if (!this.setPendingOptionsAndResult(options, null, result)) {
284 this.finishWithAlreadyActiveError(result); 284 this.finishWithAlreadyActiveError(result);
285 return; 285 return;
286 } 286 }
287 287
288 - this.chooseMedia(9, 'handleChooseMediaResult', photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE) 288 + this.chooseMedia(limit, 'handleChooseMediaResult', photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE)
289 } 289 }
290 290
291 // 唤起相机拍照 291 // 唤起相机拍照
@@ -95,7 +95,7 @@ export default class ImagePickerPlugin implements FlutterPlugin, AbilityAware { @@ -95,7 +95,7 @@ export default class ImagePickerPlugin implements FlutterPlugin, AbilityAware {
95 95
96 this.setCameraDevice(delegate, source); 96 this.setCameraDevice(delegate, source);
97 if (generalOptions.getAllowMultiple()) { 97 if (generalOptions.getAllowMultiple()) {
98 - delegate.chooseMultiImagesFromGallery(options, generalOptions.getUsePhotoPicker(), result); 98 + delegate.chooseMultiImagesFromGallery(options,generalOptions.getLimit(), generalOptions.getUsePhotoPicker(), result);
99 } else { 99 } else {
100 switch (source.getType()) { 100 switch (source.getType()) {
101 case SourceType.GALLERY: { 101 case SourceType.GALLERY: {
@@ -60,11 +60,14 @@ export class FlutterError extends Error { @@ -60,11 +60,14 @@ export class FlutterError extends Error {
60 class GeneralOptionsBuilder { 60 class GeneralOptionsBuilder {
61 setAllowMultiple: (setterArg: boolean) => ESObject 61 setAllowMultiple: (setterArg: boolean) => ESObject
62 setUsePhotoPicker: (setterArg: boolean) => ESObject 62 setUsePhotoPicker: (setterArg: boolean) => ESObject
  63 + setLimit: (setterArg: number) => ESObject
  64 +
63 build: () => ESObject 65 build: () => ESObject
64 66
65 - constructor(setAllowMultiple: (setterArg: boolean) => ESObject, setUsePhotoPicker: (setterArg: boolean) => ESObject, build: () => ESObject) { 67 + constructor(setAllowMultiple: (setterArg: boolean) => ESObject, setUsePhotoPicker: (setterArg: boolean) => ESObject,setLimit: (setterArg: number) => ESObject, build: () => ESObject) {
66 this.setAllowMultiple = setAllowMultiple 68 this.setAllowMultiple = setAllowMultiple
67 this.setUsePhotoPicker = setUsePhotoPicker 69 this.setUsePhotoPicker = setUsePhotoPicker
  70 + this.setLimit = setLimit
68 this.build = build 71 this.build = build
69 } 72 }
70 } 73 }
@@ -72,6 +75,7 @@ class GeneralOptionsBuilder { @@ -72,6 +75,7 @@ class GeneralOptionsBuilder {
72 export class GeneralOptions { 75 export class GeneralOptions {
73 private allowMultiple: boolean = false; 76 private allowMultiple: boolean = false;
74 private usePhotoPicker: boolean = false; 77 private usePhotoPicker: boolean = false;
  78 + private limit: number = 1;
75 79
76 private constructor() { 80 private constructor() {
77 } 81 }
@@ -98,16 +102,33 @@ export class GeneralOptions { @@ -98,16 +102,33 @@ export class GeneralOptions {
98 this.usePhotoPicker = setterArg; 102 this.usePhotoPicker = setterArg;
99 } 103 }
100 104
  105 +
  106 + getLimit(): number {
  107 + return this.limit;
  108 + }
  109 +
  110 + setLimit(setterArg: number): void {
  111 + if (setterArg == null) {
  112 + throw new Error("Nonnull field \"limit\" is null.");
  113 + }
  114 + this.limit = setterArg;
  115 + }
  116 +
  117 +
101 public Builder: ESObject = new GeneralOptionsBuilder((setterArg: boolean) => { 118 public Builder: ESObject = new GeneralOptionsBuilder((setterArg: boolean) => {
102 this.allowMultiple = setterArg; 119 this.allowMultiple = setterArg;
103 return this; 120 return this;
104 }, (setterArg: boolean) => { 121 }, (setterArg: boolean) => {
105 this.usePhotoPicker = setterArg; 122 this.usePhotoPicker = setterArg;
106 return this; 123 return this;
107 - }, (): ESObject => { 124 + }, (setterArg: number) => {
  125 + this.limit = setterArg;
  126 + return this;
  127 + }, (): ESObject => {
108 const pigeonReturn: ESObject = new GeneralOptions(); 128 const pigeonReturn: ESObject = new GeneralOptions();
109 pigeonReturn.setAllowMultiple(this.allowMultiple); 129 pigeonReturn.setAllowMultiple(this.allowMultiple);
110 pigeonReturn.setUsePhotoPicker(this.usePhotoPicker); 130 pigeonReturn.setUsePhotoPicker(this.usePhotoPicker);
  131 + pigeonReturn.setUsePhotoPicker(this.limit);
111 return pigeonReturn; 132 return pigeonReturn;
112 } 133 }
113 ) 134 )
@@ -116,6 +137,7 @@ export class GeneralOptions { @@ -116,6 +137,7 @@ export class GeneralOptions {
116 const toListResult: ArrayList<ESObject> = new ArrayList<ESObject>(); 137 const toListResult: ArrayList<ESObject> = new ArrayList<ESObject>();
117 toListResult.add(this.allowMultiple); 138 toListResult.add(this.allowMultiple);
118 toListResult.add(this.usePhotoPicker); 139 toListResult.add(this.usePhotoPicker);
  140 + toListResult.add(this.limit);
119 return toListResult; 141 return toListResult;
120 } 142 }
121 143
@@ -125,6 +147,8 @@ export class GeneralOptions { @@ -125,6 +147,8 @@ export class GeneralOptions {
125 pigeonResult.setAllowMultiple(allowMultiple); 147 pigeonResult.setAllowMultiple(allowMultiple);
126 const usePhotoPicker: ESObject = list[1]; 148 const usePhotoPicker: ESObject = list[1];
127 pigeonResult.setUsePhotoPicker(usePhotoPicker); 149 pigeonResult.setUsePhotoPicker(usePhotoPicker);
  150 + const limit: ESObject = list[2];
  151 + pigeonResult.setLimit(limit);
128 return pigeonResult; 152 return pigeonResult;
129 } 153 }
130 } 154 }
@@ -58,6 +58,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @@ -58,6 +58,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
58 double? maxHeight, 58 double? maxHeight,
59 int? imageQuality, 59 int? imageQuality,
60 bool requestFullMetadata = true, 60 bool requestFullMetadata = true,
  61 + int? limit,
61 }) { 62 }) {
62 if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { 63 if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
63 throw ArgumentError.value( 64 throw ArgumentError.value(
@@ -79,6 +80,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @@ -79,6 +80,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
79 'maxHeight': maxHeight, 80 'maxHeight': maxHeight,
80 'imageQuality': imageQuality, 81 'imageQuality': imageQuality,
81 'requestFullMetadata': requestFullMetadata, 82 'requestFullMetadata': requestFullMetadata,
  83 + 'limit': limit,
82 }, 84 },
83 ); 85 );
84 } 86 }
@@ -244,6 +246,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @@ -244,6 +246,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
244 maxHeight: options.imageOptions.maxHeight, 246 maxHeight: options.imageOptions.maxHeight,
245 imageQuality: options.imageOptions.imageQuality, 247 imageQuality: options.imageOptions.imageQuality,
246 requestFullMetadata: options.imageOptions.requestFullMetadata, 248 requestFullMetadata: options.imageOptions.requestFullMetadata,
  249 + limit: options.limit,
247 ); 250 );
248 if (paths == null) { 251 if (paths == null) {
249 return <XFile>[]; 252 return <XFile>[];
@@ -263,6 +266,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @@ -263,6 +266,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
263 'maxImageHeight': imageOptions.maxHeight, 266 'maxImageHeight': imageOptions.maxHeight,
264 'imageQuality': imageOptions.imageQuality, 267 'imageQuality': imageOptions.imageQuality,
265 'allowMultiple': options.allowMultiple, 268 'allowMultiple': options.allowMultiple,
  269 + 'limit': options.limit,
266 }; 270 };
267 271
268 final List<XFile>? paths = await _channel 272 final List<XFile>? paths = await _channel
@@ -312,13 +316,10 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @@ -312,13 +316,10 @@ class MethodChannelImagePicker extends ImagePickerPlatform {
312 switch (type) { 316 switch (type) {
313 case kTypeImage: 317 case kTypeImage:
314 retrieveType = RetrieveType.image; 318 retrieveType = RetrieveType.image;
315 - break;  
316 case kTypeVideo: 319 case kTypeVideo:
317 retrieveType = RetrieveType.video; 320 retrieveType = RetrieveType.video;
318 - break;  
319 case kTypeMedia: 321 case kTypeMedia:
320 retrieveType = RetrieveType.media; 322 retrieveType = RetrieveType.media;
321 - break;  
322 } 323 }
323 324
324 PlatformException? exception; 325 PlatformException? exception;
@@ -13,11 +13,48 @@ class MediaOptions { @@ -13,11 +13,48 @@ class MediaOptions {
13 const MediaOptions({ 13 const MediaOptions({
14 this.imageOptions = const ImageOptions(), 14 this.imageOptions = const ImageOptions(),
15 required this.allowMultiple, 15 required this.allowMultiple,
  16 + this.limit,
16 }); 17 });
17 18
  19 + /// Construct a new MediaOptions instance.
  20 + ///
  21 + /// Throws if limit is lower than 2,
  22 + /// or allowMultiple is false and limit is not null.
  23 + MediaOptions.createAndValidate({
  24 + this.imageOptions = const ImageOptions(),
  25 + required this.allowMultiple,
  26 + this.limit,
  27 + }) {
  28 + _validate(allowMultiple: allowMultiple, limit: limit);
  29 + }
  30 +
18 /// Options that will apply to images upon selection. 31 /// Options that will apply to images upon selection.
19 final ImageOptions imageOptions; 32 final ImageOptions imageOptions;
20 33
21 /// Whether to allow for selecting multiple media. 34 /// Whether to allow for selecting multiple media.
22 final bool allowMultiple; 35 final bool allowMultiple;
  36 +
  37 + /// The maximum number of images to select.
  38 + ///
  39 + /// Default null value means no limit.
  40 + /// This value may be ignored by platforms that cannot support it.
  41 + final int? limit;
  42 +
  43 + /// Validates that all values are within required ranges.
  44 + ///
  45 + /// Throws if limit is lower than 2,
  46 + /// or allowMultiple is false and limit is not null.
  47 + static void _validate({required bool allowMultiple, int? limit}) {
  48 + if (!allowMultiple && limit != null) {
  49 + throw ArgumentError.value(
  50 + allowMultiple,
  51 + 'allowMultiple',
  52 + 'cannot be false, when limit is not null',
  53 + );
  54 + }
  55 +
  56 + if (limit != null && limit < 2) {
  57 + throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2');
  58 + }
  59 + }
23 } 60 }
@@ -6,11 +6,37 @@ import 'image_options.dart'; @@ -6,11 +6,37 @@ import 'image_options.dart';
6 6
7 /// Specifies options for picking multiple images from the device's gallery. 7 /// Specifies options for picking multiple images from the device's gallery.
8 class MultiImagePickerOptions { 8 class MultiImagePickerOptions {
9 - /// Creates an instance with the given [imageOptions]. 9 + /// Creates an instance with the given [imageOptions] and [limit].
10 const MultiImagePickerOptions({ 10 const MultiImagePickerOptions({
11 this.imageOptions = const ImageOptions(), 11 this.imageOptions = const ImageOptions(),
  12 + this.limit,
12 }); 13 });
13 14
  15 + /// Creates an instance with the given [imageOptions] and [limit].
  16 + ///
  17 + /// Throws if limit is lower than 2.
  18 + MultiImagePickerOptions.createAndValidate({
  19 + this.imageOptions = const ImageOptions(),
  20 + this.limit,
  21 + }) {
  22 + _validate(limit: limit);
  23 + }
  24 +
14 /// The image-specific options for picking. 25 /// The image-specific options for picking.
15 final ImageOptions imageOptions; 26 final ImageOptions imageOptions;
  27 +
  28 + /// The maximum number of images to select.
  29 + ///
  30 + /// Default null value means no limit.
  31 + /// This value may be ignored by platforms that cannot support it.
  32 + final int? limit;
  33 +
  34 + /// Validates that all values are within required ranges.
  35 + ///
  36 + /// Throws if limit is lower than 2.
  37 + static void _validate({int? limit}) {
  38 + if (limit != null && limit < 2) {
  39 + throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2');
  40 + }
  41 + }
16 } 42 }