Showing
18 changed files
with
578 additions
and
172 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 | - | 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") | ||
| 18 | List<String> attributes = | 27 | List<String> attributes = |
| 19 | Arrays.asList( | 28 | 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"); | 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); | ||
| 39 | for (String attribute : attributes) { | 134 | for (String attribute : attributes) { |
| 40 | setIfNotNull(oldExif, newExif, attribute); | 135 | setIfNotNull(oldExif, newExif, attribute); |
| 41 | } | 136 | } |
| 42 | 137 | ||
| 43 | newExif.saveAttributes(); | 138 | newExif.saveAttributes(); |
| 44 | - | ||
| 45 | - } catch (Exception ex) { | ||
| 46 | - Log.e("ExifDataCopier", "Error preserving Exif data on selected image: " + ex); | ||
| 47 | - } | ||
| 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,10 +322,8 @@ public class ImagePickerDelegate | @@ -319,10 +322,8 @@ 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()); | 325 | pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple()); |
| 324 | } | 326 | } |
| 325 | - } | ||
| 326 | activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY); | 327 | activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY); |
| 327 | } | 328 | } |
| 328 | 329 | ||
| @@ -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,10 +468,8 @@ public class ImagePickerDelegate | @@ -466,10 +468,8 @@ 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 | pickMultiImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); |
| 471 | } | 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); |
| 475 | } | 475 | } |
| @@ -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,21 +123,15 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic | @@ -125,21 +123,15 @@ 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 { | 128 | + |
| 136 | // V2 embedding setup for activity listeners. | 129 | // V2 embedding setup for activity listeners. |
| 137 | activityBinding.addActivityResultListener(delegate); | 130 | activityBinding.addActivityResultListener(delegate); |
| 138 | activityBinding.addRequestPermissionsResultListener(delegate); | 131 | activityBinding.addRequestPermissionsResultListener(delegate); |
| 139 | lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding); | 132 | lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding); |
| 140 | lifecycle.addObserver(observer); | 133 | lifecycle.addObserver(observer); |
| 141 | } | 134 | } |
| 142 | - } | ||
| 143 | 135 | ||
| 144 | // Only invoked by {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing. | 136 | // Only invoked by {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing. |
| 145 | ActivityState(final ImagePickerDelegate delegate, final Activity activity) { | 137 | ActivityState(final ImagePickerDelegate delegate, final Activity activity) { |
| @@ -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; | 103 | + double WidthForMaxHeight = height * aspectRatio; |
| 104 | + double heightForMaxWidth = width / aspectRatio; | ||
| 102 | 105 | ||
| 103 | - if (width < height) { | ||
| 104 | - if (!hasMaxWidth) { | ||
| 105 | - width = downscaledWidth; | 106 | + if (heightForMaxWidth > height) { |
| 107 | + width = (double) Math.round(WidthForMaxHeight); | ||
| 106 | } else { | 108 | } else { |
| 107 | - height = downscaledHeight; | ||
| 108 | - } | ||
| 109 | - } else if (height < width) { | ||
| 110 | - if (!hasMaxHeight) { | ||
| 111 | - height = downscaledHeight; | ||
| 112 | - } else { | ||
| 113 | - width = downscaledWidth; | ||
| 114 | - } | ||
| 115 | - } 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,15 +207,30 @@ class ImagePickerOhos extends ImagePickerPlatform { | @@ -199,15 +207,30 @@ 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 | - ), | 233 | + _mediaOptionsToGeneralOptions(options), |
| 211 | )) | 234 | )) |
| 212 | .map((String? path) => XFile(path!)) | 235 | .map((String? path) => XFile(path!)) |
| 213 | .toList(); | 236 | .toList(); |
| @@ -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; |
| 124 | + }, (setterArg: number) => { | ||
| 125 | + this.limit = setterArg; | ||
| 126 | + return this; | ||
| 107 | }, (): ESObject => { | 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