Showing
18 changed files
with
596 additions
and
190 deletions
@@ -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 | } |
-
Please register or login to post a comment