diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 90f2406dacf..c9f4b3b43fc 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.1.0 + +* Adds limit parameter to `MediaOptions` and `MultiImagePickerOptions` which limits + the number of media that can be selected. + * Currently supported only on iOS and Android. +* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3. + ## 1.0.8 * Updates minimum supported SDK version to Flutter 3.13/Dart 3.1. diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 2e1a3d720e2..a836c0a8433 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -56,6 +56,7 @@ class _MyHomePageState extends State { final TextEditingController maxWidthController = TextEditingController(); final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); + final TextEditingController limitController = TextEditingController(); Future _playVideo(XFile? file) async { if (file != null && mounted) { @@ -96,19 +97,21 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, true, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final List pickedFileList = isMedia ? await _picker.pickMultipleMedia( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, + limit: limit, ) : await _picker.pickMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, + limit: limit, ); setState(() { _mediaFileList = pickedFileList; @@ -120,8 +123,8 @@ class _MyHomePageState extends State { } }); } else if (isMedia) { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, false, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final List pickedFileList = []; final XFile? media = await _picker.pickMedia( @@ -142,8 +145,8 @@ class _MyHomePageState extends State { } }); } else { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, false, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final XFile? pickedFile = await _picker.pickImage( source: source, @@ -454,7 +457,7 @@ class _MyHomePageState extends State { } Future _displayPickImageDialog( - BuildContext context, OnPickImageCallback onPick) async { + BuildContext context, bool isMulti, OnPickImageCallback onPick) async { return showDialog( context: context, builder: (BuildContext context) { @@ -483,6 +486,13 @@ class _MyHomePageState extends State { decoration: const InputDecoration( hintText: 'Enter quality if desired'), ), + if (isMulti) + TextField( + controller: limitController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter limit if desired'), + ), ], ), actions: [ @@ -504,7 +514,10 @@ class _MyHomePageState extends State { final int? quality = qualityController.text.isNotEmpty ? int.parse(qualityController.text) : null; - onPick(width, height, quality); + final int? limit = limitController.text.isNotEmpty + ? int.parse(limitController.text) + : null; + onPick(width, height, quality, limit); Navigator.of(context).pop(); }), ], @@ -514,7 +527,7 @@ class _MyHomePageState extends State { } typedef OnPickImageCallback = void Function( - double? maxWidth, double? maxHeight, int? quality); + double? maxWidth, double? maxHeight, int? quality, int? limit); class AspectRatioVideo extends StatefulWidget { const AspectRatioVideo(this.controller, {super.key}); diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index b41127efc89..b7ca08c9715 100644 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the image_picker plugin. publish_to: none environment: - sdk: ^3.1.0 - flutter: ">=3.13.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" dependencies: flutter: @@ -17,7 +17,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.10.0 mime: ^1.0.4 video_player: ^2.7.0 diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 0ad4afc74ef..246fe3a4ca1 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -128,6 +128,7 @@ class ImagePicker { double? maxWidth, double? maxHeight, int? imageQuality, + int? limit, bool requestFullMetadata = true, }) { final ImageOptions imageOptions = ImageOptions.createAndValidate( @@ -138,8 +139,9 @@ class ImagePicker { ); return platform.getMultiImageWithOptions( - options: MultiImagePickerOptions( + options: MultiImagePickerOptions.createAndValidate( imageOptions: imageOptions, + limit: limit, ), ); } @@ -186,7 +188,7 @@ class ImagePicker { bool requestFullMetadata = true, }) async { final List listMedia = await platform.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( imageOptions: ImageOptions.createAndValidate( maxHeight: maxHeight, maxWidth: maxWidth, @@ -239,10 +241,11 @@ class ImagePicker { double? maxWidth, double? maxHeight, int? imageQuality, + int? limit, bool requestFullMetadata = true, }) { return platform.getMedia( - options: MediaOptions( + options: MediaOptions.createAndValidate( allowMultiple: true, imageOptions: ImageOptions.createAndValidate( maxHeight: maxHeight, @@ -250,6 +253,7 @@ class ImagePicker { imageQuality: imageQuality, requestFullMetadata: requestFullMetadata, ), + limit: limit, ), ); } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index a0e3ce1dece..5854c9f0211 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 1.0.8 +version: 1.1.0 environment: - sdk: ^3.1.0 - flutter: ">=3.13.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" flutter: plugin: @@ -30,10 +30,10 @@ dependencies: sdk: flutter image_picker_android: ^0.8.7 image_picker_for_web: ">=2.2.0 <4.0.0" - image_picker_ios: ^0.8.9+1 + image_picker_ios: ^0.8.8 image_picker_linux: ^0.2.1 image_picker_macos: ^0.2.1 - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.10.0 image_picker_windows: ^0.2.1 dev_dependencies: diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 4ff5b4e025d..766b3808998 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -431,7 +431,16 @@ void main() { imageQuality: 70, ); await picker.pickMultiImage( - maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + limit: 5, + ); verifyInOrder([ mockPlatform.getMultiImageWithOptions( @@ -529,6 +538,29 @@ void main() { named: 'options', ), ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf() + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)) + .having((MultiImagePickerOptions options) => options.limit, + 'limit', equals(5)), + named: 'options', + ), + ), ]); }); @@ -545,6 +577,24 @@ void main() { ); }); + test('does not accept a limit argument lower than 2', () { + final ImagePicker picker = ImagePicker(); + expect( + () => picker.pickMultiImage(limit: -1), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(limit: 0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(limit: 1), + throwsArgumentError, + ); + }); + test('handles an empty image file response gracefully', () async { final ImagePicker picker = ImagePicker(); @@ -620,7 +670,15 @@ void main() { imageQuality: 70, ); await picker.pickMedia( - maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + await picker.pickMedia( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); verifyInOrder([ mockPlatform.getMedia( @@ -793,7 +851,16 @@ void main() { imageQuality: 70, ); await picker.pickMultipleMedia( - maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + limit: 5, + ); verifyInOrder([ mockPlatform.getMedia( @@ -885,6 +952,27 @@ void main() { named: 'options', ), ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)) + .having((MediaOptions options) => options.limit, 'limit', + equals(5)), + named: 'options', + ), + ), ]); }); @@ -901,6 +989,24 @@ void main() { ); }); + test('does not accept a limit argument lower than 2', () { + final ImagePicker picker = ImagePicker(); + expect( + () => picker.pickMultipleMedia(limit: -1), + throwsArgumentError, + ); + + expect( + () => picker.pickMultipleMedia(limit: 0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultipleMedia(limit: 1), + throwsArgumentError, + ); + }); + test('handles an empty image file response gracefully', () async { final ImagePicker picker = ImagePicker(); diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 5ffcad5de19..9efc86c1c93 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.10 + +* Adds limit parameter to `MediaOptions` and `MultiImagePickerOptions` that sets a limit to how many media or image items can be selected. + ## 0.8.9+6 * Updates minSdkVersion to 19. diff --git a/packages/image_picker/image_picker_android/android/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_android/android/src/main/AndroidManifest.xml index 5d1773ee03a..5f9bcd822e5 100755 --- a/packages/image_picker/image_picker_android/android/src/main/AndroidManifest.xml +++ b/packages/image_picker/image_picker_android/android/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ @@ -11,5 +12,15 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/flutter_image_picker_file_paths" /> + + + + + + + diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index 98b55608a75..96bf727a881 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -297,8 +297,10 @@ private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOpt Intent pickMediaIntent; if (generalOptions.getUsePhotoPicker()) { if (generalOptions.getAllowMultiple()) { + int limit = ImagePickerUtils.getLimitFromOption(generalOptions); + pickMediaIntent = - new ActivityResultContracts.PickMultipleVisualMedia() + new ActivityResultContracts.PickMultipleVisualMedia(limit) .createIntent( activity, new PickVisualMediaRequest.Builder() @@ -426,13 +428,14 @@ public void chooseImageFromGallery( public void chooseMultiImageFromGallery( @NonNull ImageSelectionOptions options, boolean usePhotoPicker, + int limit, @NonNull Messages.Result> result) { if (!setPendingOptionsAndResult(options, null, result)) { finishWithAlreadyActiveError(result); return; } - launchMultiPickImageFromGalleryIntent(usePhotoPicker); + launchMultiPickImageFromGalleryIntent(usePhotoPicker, limit); } private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) { @@ -452,11 +455,11 @@ private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) { activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY); } - private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker) { + private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker, int limit) { Intent pickMultiImageIntent; if (usePhotoPicker) { pickMultiImageIntent = - new ActivityResultContracts.PickMultipleVisualMedia() + new ActivityResultContracts.PickMultipleVisualMedia(limit) .createIntent( activity, new PickVisualMediaRequest.Builder() diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index b5deb289341..8096930e7d7 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -125,7 +125,7 @@ private class ActivityState { this.messenger = messenger; delegate = constructDelegate(activity); - ImagePickerApi.setup(messenger, handler); + ImagePickerApi.setUp(messenger, handler); observer = new LifeCycleObserver(activity); if (registrar != null) { // V1 embedding setup for activity listeners. @@ -159,7 +159,7 @@ void release() { lifecycle = null; } - ImagePickerApi.setup(messenger, null); + ImagePickerApi.setUp(messenger, null); if (application != null) { application.unregisterActivityLifecycleCallbacks(observer); @@ -317,7 +317,10 @@ public void pickImages( setCameraDevice(delegate, source); if (generalOptions.getAllowMultiple()) { - delegate.chooseMultiImageFromGallery(options, generalOptions.getUsePhotoPicker(), result); + int limit = ImagePickerUtils.getLimitFromOption(generalOptions); + + delegate.chooseMultiImageFromGallery( + options, generalOptions.getUsePhotoPicker(), limit, result); } else { switch (source.getType()) { case GALLERY: diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java index 6a93c69feb4..d14056423c4 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java @@ -5,10 +5,13 @@ package io.flutter.plugins.imagepicker; import android.Manifest; +import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.provider.MediaStore; +import androidx.activity.result.contract.ActivityResultContracts; import java.util.Arrays; final class ImagePickerUtils { @@ -54,4 +57,32 @@ static boolean needRequestCameraPermission(Context context) { boolean greatOrEqualM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; return greatOrEqualM && isPermissionPresentInManifest(context, Manifest.permission.CAMERA); } + + /** + * The system photo picker has a maximum limit of selectable items returned by + * [MediaStore.getPickImagesMaxLimit()] On devices supporting picker provided via + * [ACTION_SYSTEM_FALLBACK_PICK_IMAGES], the limit may be ignored if it's higher than the allowed + * limit. On devices not supporting the photo picker, the limit is ignored. + * + * @see MediaStore.EXTRA_PICK_IMAGES_MAX + */ + @SuppressLint({"NewApi", "ClassVerificationFailure"}) + static int getMaxItems() { + if (ActivityResultContracts.PickVisualMedia.isSystemPickerAvailable$activity_release()) { + return MediaStore.getPickImagesMaxLimit(); + } else { + return Integer.MAX_VALUE; + } + } + + static int getLimitFromOption(Messages.GeneralOptions generalOptions) { + Long limit = generalOptions.getLimit(); + int effectiveLimit = getMaxItems(); + + if (limit != null && limit < effectiveLimit) { + effectiveLimit = Math.toIntExact(limit); + } + + return effectiveLimit; + } } diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java index 8a19cfd3c55..b987a2284a8 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java @@ -1,11 +1,14 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// Autogenerated from Pigeon (v17.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.imagepicker; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.CLASS; + import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,6 +17,8 @@ import io.flutter.plugin.common.MessageCodec; import io.flutter.plugin.common.StandardMessageCodec; import java.io.ByteArrayOutputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -55,6 +60,10 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { return errorList; } + @Target(METHOD) + @Retention(CLASS) + @interface CanIgnoreReturnValue {} + public enum SourceCamera { REAR(0), FRONT(1); @@ -116,6 +125,16 @@ public void setUsePhotoPicker(@NonNull Boolean setterArg) { this.usePhotoPicker = setterArg; } + private @Nullable Long limit; + + public @Nullable Long getLimit() { + return limit; + } + + public void setLimit(@Nullable Long setterArg) { + this.limit = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ GeneralOptions() {} @@ -123,6 +142,7 @@ public static final class Builder { private @Nullable Boolean allowMultiple; + @CanIgnoreReturnValue public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) { this.allowMultiple = setterArg; return this; @@ -130,24 +150,35 @@ public static final class Builder { private @Nullable Boolean usePhotoPicker; + @CanIgnoreReturnValue public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) { this.usePhotoPicker = setterArg; return this; } + private @Nullable Long limit; + + @CanIgnoreReturnValue + public @NonNull Builder setLimit(@Nullable Long setterArg) { + this.limit = setterArg; + return this; + } + public @NonNull GeneralOptions build() { GeneralOptions pigeonReturn = new GeneralOptions(); pigeonReturn.setAllowMultiple(allowMultiple); pigeonReturn.setUsePhotoPicker(usePhotoPicker); + pigeonReturn.setLimit(limit); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList(2); + ArrayList toListResult = new ArrayList(3); toListResult.add(allowMultiple); toListResult.add(usePhotoPicker); + toListResult.add(limit); return toListResult; } @@ -157,6 +188,9 @@ ArrayList toList() { pigeonResult.setAllowMultiple((Boolean) allowMultiple); Object usePhotoPicker = list.get(1); pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker); + Object limit = list.get(2); + pigeonResult.setLimit( + (limit == null) ? null : ((limit instanceof Integer) ? (Integer) limit : (Long) limit)); return pigeonResult; } } @@ -214,6 +248,7 @@ public static final class Builder { private @Nullable Double maxWidth; + @CanIgnoreReturnValue public @NonNull Builder setMaxWidth(@Nullable Double setterArg) { this.maxWidth = setterArg; return this; @@ -221,6 +256,7 @@ public static final class Builder { private @Nullable Double maxHeight; + @CanIgnoreReturnValue public @NonNull Builder setMaxHeight(@Nullable Double setterArg) { this.maxHeight = setterArg; return this; @@ -228,6 +264,7 @@ public static final class Builder { private @Nullable Long quality; + @CanIgnoreReturnValue public @NonNull Builder setQuality(@NonNull Long setterArg) { this.quality = setterArg; return this; @@ -288,6 +325,7 @@ public static final class Builder { private @Nullable ImageSelectionOptions imageSelectionOptions; + @CanIgnoreReturnValue public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) { this.imageSelectionOptions = setterArg; return this; @@ -339,6 +377,7 @@ public static final class Builder { private @Nullable Long maxDurationSeconds; + @CanIgnoreReturnValue public @NonNull Builder setMaxDurationSeconds(@Nullable Long setterArg) { this.maxDurationSeconds = setterArg; return this; @@ -407,6 +446,7 @@ public static final class Builder { private @Nullable SourceType type; + @CanIgnoreReturnValue public @NonNull Builder setType(@NonNull SourceType setterArg) { this.type = setterArg; return this; @@ -414,6 +454,7 @@ public static final class Builder { private @Nullable SourceCamera camera; + @CanIgnoreReturnValue public @NonNull Builder setCamera(@Nullable SourceCamera setterArg) { this.camera = setterArg; return this; @@ -438,7 +479,7 @@ ArrayList toList() { static @NonNull SourceSpecification fromList(@NonNull ArrayList list) { SourceSpecification pigeonResult = new SourceSpecification(); Object type = list.get(0); - pigeonResult.setType(type == null ? null : SourceType.values()[(int) type]); + pigeonResult.setType(SourceType.values()[(int) type]); Object camera = list.get(1); pigeonResult.setCamera(camera == null ? null : SourceCamera.values()[(int) camera]); return pigeonResult; @@ -483,6 +524,7 @@ public static final class Builder { private @Nullable String code; + @CanIgnoreReturnValue public @NonNull Builder setCode(@NonNull String setterArg) { this.code = setterArg; return this; @@ -490,6 +532,7 @@ public static final class Builder { private @Nullable String message; + @CanIgnoreReturnValue public @NonNull Builder setMessage(@Nullable String setterArg) { this.message = setterArg; return this; @@ -578,6 +621,7 @@ public static final class Builder { private @Nullable CacheRetrievalType type; + @CanIgnoreReturnValue public @NonNull Builder setType(@NonNull CacheRetrievalType setterArg) { this.type = setterArg; return this; @@ -585,6 +629,7 @@ public static final class Builder { private @Nullable CacheRetrievalError error; + @CanIgnoreReturnValue public @NonNull Builder setError(@Nullable CacheRetrievalError setterArg) { this.error = setterArg; return this; @@ -592,6 +637,7 @@ public static final class Builder { private @Nullable List paths; + @CanIgnoreReturnValue public @NonNull Builder setPaths(@NonNull List setterArg) { this.paths = setterArg; return this; @@ -618,7 +664,7 @@ ArrayList toList() { static @NonNull CacheRetrievalResult fromList(@NonNull ArrayList list) { CacheRetrievalResult pigeonResult = new CacheRetrievalResult(); Object type = list.get(0); - pigeonResult.setType(type == null ? null : CacheRetrievalType.values()[(int) type]); + pigeonResult.setType(CacheRetrievalType.values()[(int) type]); Object error = list.get(1); pigeonResult.setError( (error == null) ? null : CacheRetrievalError.fromList((ArrayList) error)); @@ -628,10 +674,28 @@ ArrayList toList() { } } + /** Asynchronous error handling return type for non-nullable API method returns. */ public interface Result { - @SuppressWarnings("UnknownNullness") - void success(T result); + /** Success case callback method for handling returns. */ + void success(@NonNull T result); + + /** Failure case callback method for handling errors. */ + void error(@NonNull Throwable error); + } + /** Asynchronous error handling return type for nullable API method returns. */ + public interface NullableResult { + /** Success case callback method for handling returns. */ + void success(@Nullable T result); + /** Failure case callback method for handling errors. */ + void error(@NonNull Throwable error); + } + /** Asynchronous error handling return type for void API method returns. */ + public interface VoidResult { + /** Success case callback method for handling returns. */ + void success(); + + /** Failure case callback method for handling errors. */ void error(@NonNull Throwable error); } @@ -734,13 +798,13 @@ void pickMedia( return ImagePickerApiCodec.INSTANCE; } /** Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. */ - static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePickerApi api) { + static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePickerApi api) { { BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.ImagePickerApi.pickImages", + "dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickImages", getCodec(), taskQueue); if (api != null) { @@ -775,7 +839,7 @@ public void error(Throwable error) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.ImagePickerApi.pickVideos", + "dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickVideos", getCodec(), taskQueue); if (api != null) { @@ -808,7 +872,9 @@ public void error(Throwable error) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.ImagePickerApi.pickMedia", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickMedia", + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -841,7 +907,7 @@ public void error(Throwable error) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.ImagePickerApi.retrieveLostResults", + "dev.flutter.pigeon.image_picker_android.ImagePickerApi.retrieveLostResults", getCodec(), taskQueue); if (api != null) { diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 92c988cd6bf..81546da52ec 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -160,7 +160,8 @@ public void chooseMultiImageFromGallery_whenPendingResultExists_finishesWithAlre ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); - delegate.chooseMultiImageFromGallery(DEFAULT_IMAGE_OPTIONS, false, mockResult); + delegate.chooseMultiImageFromGallery( + DEFAULT_IMAGE_OPTIONS, false, Integer.MAX_VALUE, mockResult); verifyFinishedWithAlreadyActiveError(); verifyNoMoreInteractions(mockResult); @@ -207,7 +208,8 @@ public void chooseImageFromGallery_withPhotoPicker_launchesChooseFromGalleryInte public void chooseMultiImageFromGallery_launchesChooseFromGalleryIntent() { ImagePickerDelegate delegate = createDelegate(); - delegate.chooseMultiImageFromGallery(DEFAULT_IMAGE_OPTIONS, true, mockResult); + delegate.chooseMultiImageFromGallery( + DEFAULT_IMAGE_OPTIONS, true, Integer.MAX_VALUE, mockResult); verify(mockActivity) .startActivityForResult( @@ -220,7 +222,8 @@ public void chooseMultiImageFromGallery_launchesChooseFromGalleryIntent() { public void chooseMultiImageFromGallery_withPhotoPicker_launchesChooseFromGalleryIntent() { ImagePickerDelegate delegate = createDelegate(); - delegate.chooseMultiImageFromGallery(DEFAULT_IMAGE_OPTIONS, false, mockResult); + delegate.chooseMultiImageFromGallery( + DEFAULT_IMAGE_OPTIONS, false, Integer.MAX_VALUE, mockResult); verify(mockActivity) .startActivityForResult( diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index b2c281ca540..cf088b59b19 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -52,6 +52,19 @@ public class ImagePickerPluginTest { new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(false).build(); private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER = new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(true).build(); + private static final GeneralOptions + GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER_WITH_LIMIT = + new GeneralOptions.Builder() + .setUsePhotoPicker(false) + .setAllowMultiple(true) + .setLimit((long) 5) + .build(); + private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER_WITH_LIMIT = + new GeneralOptions.Builder() + .setUsePhotoPicker(true) + .setAllowMultiple(true) + .setLimit((long) 5) + .build(); private static final SourceSpecification SOURCE_GALLERY = new SourceSpecification.Builder().setType(Messages.SourceType.GALLERY).build(); private static final SourceSpecification SOURCE_CAMERA_FRONT = @@ -171,7 +184,8 @@ public void pickImages_invokesChooseMultiImageFromGallery() { DEFAULT_IMAGE_OPTIONS, GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, mockResult); - verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(false), any()); + verify(mockImagePickerDelegate) + .chooseMultiImageFromGallery(any(), eq(false), eq(Integer.MAX_VALUE), any()); verifyNoInteractions(mockResult); } @@ -182,7 +196,30 @@ public void pickImages_usingPhotoPicker_invokesChooseMultiImageFromGallery() { DEFAULT_IMAGE_OPTIONS, GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER, mockResult); - verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(true), any()); + verify(mockImagePickerDelegate) + .chooseMultiImageFromGallery(any(), eq(true), eq(Integer.MAX_VALUE), any()); + verifyNoInteractions(mockResult); + } + + @Test + public void pickImages_usingPhotoPicker_withLimit5_invokesChooseMultiImageFromGallery() { + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER_WITH_LIMIT, + mockResult); + verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(true), eq(5), any()); + verifyNoInteractions(mockResult); + } + + @Test + public void pickImages_withLimit5_invokesChooseMultiImageFromGallery() { + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER_WITH_LIMIT, + mockResult); + verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(false), eq(5), any()); verifyNoInteractions(mockResult); } diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart index 94cbd7dcc3a..8218f2d5ae2 100755 --- a/packages/image_picker/image_picker_android/example/lib/main.dart +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -74,6 +74,7 @@ class _MyHomePageState extends State { final TextEditingController maxWidthController = TextEditingController(); final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); + final TextEditingController limitController = TextEditingController(); Future _playVideo(XFile? file) async { if (file != null && mounted) { @@ -109,8 +110,8 @@ class _MyHomePageState extends State { } await _playVideo(file); } else if (isMultiImage) { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, true, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final ImageOptions imageOptions = ImageOptions( maxWidth: maxWidth, @@ -120,12 +121,15 @@ class _MyHomePageState extends State { final List pickedFileList = isMedia ? await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, - imageOptions: imageOptions), + allowMultiple: isMultiImage, + imageOptions: imageOptions, + limit: limit, + ), ) : await _picker.getMultiImageWithOptions( options: MultiImagePickerOptions( imageOptions: imageOptions, + limit: limit, ), ); if (pickedFileList.isNotEmpty && context.mounted) { @@ -141,8 +145,8 @@ class _MyHomePageState extends State { } }); } else if (isMedia) { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, false, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final List pickedFileList = []; final XFile? media = _firstOrNull(await _picker.getMedia( @@ -166,8 +170,8 @@ class _MyHomePageState extends State { } }); } else { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, false, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final XFile? pickedFile = await _picker.getImageFromSource( source: source, @@ -481,7 +485,7 @@ class _MyHomePageState extends State { } Future _displayPickImageDialog( - BuildContext context, OnPickImageCallback onPick) async { + BuildContext context, bool isMulti, OnPickImageCallback onPick) async { return showDialog( context: context, builder: (BuildContext context) { @@ -509,6 +513,13 @@ class _MyHomePageState extends State { decoration: const InputDecoration( hintText: 'Enter quality if desired'), ), + if (isMulti) + TextField( + controller: limitController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter limit if desired'), + ), ], ), actions: [ @@ -530,7 +541,10 @@ class _MyHomePageState extends State { final int? quality = qualityController.text.isNotEmpty ? int.parse(qualityController.text) : null; - onPick(width, height, quality); + final int? limit = limitController.text.isNotEmpty + ? int.parse(limitController.text) + : null; + onPick(width, height, quality, limit); Navigator.of(context).pop(); }), ], @@ -547,7 +561,7 @@ class _MyHomePageState extends State { } typedef OnPickImageCallback = void Function( - double? maxWidth, double? maxHeight, int? quality); + double? maxWidth, double? maxHeight, int? quality, int? limit); class AspectRatioVideo extends StatefulWidget { const AspectRatioVideo(this.controller, {super.key}); diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml index c2fa9e98131..500b47c3578 100644 --- a/packages/image_picker/image_picker_android/example/pubspec.yaml +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the image_picker plugin. publish_to: none environment: - sdk: ^3.1.0 - flutter: ">=3.13.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" dependencies: flutter: @@ -19,7 +19,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.10.0 mime: ^1.0.4 video_player: ^2.1.4 diff --git a/packages/image_picker/image_picker_android/lib/image_picker_android.dart b/packages/image_picker/image_picker_android/lib/image_picker_android.dart index c9e2c875e8b..2bcebe26802 100644 --- a/packages/image_picker/image_picker_android/lib/image_picker_android.dart +++ b/packages/image_picker/image_picker_android/lib/image_picker_android.dart @@ -67,6 +67,7 @@ class ImagePickerAndroid extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + int? limit, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( @@ -81,6 +82,10 @@ class ImagePickerAndroid extends ImagePickerPlatform { throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); } + if (limit != null && limit < 2) { + throw ArgumentError.value(limit, 'limit', 'cannot be lower than 2'); + } + return _hostApi.pickImages( SourceSpecification(type: SourceType.gallery), ImageSelectionOptions( @@ -88,7 +93,10 @@ class ImagePickerAndroid extends ImagePickerPlatform { maxHeight: maxHeight, quality: imageQuality ?? 100), GeneralOptions( - allowMultiple: true, usePhotoPicker: useAndroidPhotoPicker), + allowMultiple: true, + usePhotoPicker: useAndroidPhotoPicker, + limit: limit, + ), ); } @@ -209,16 +217,31 @@ class ImagePickerAndroid extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMultiImageWithOptions({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + final List paths = await _getMultiImagePath( + maxWidth: options.imageOptions.maxWidth, + maxHeight: options.imageOptions.maxHeight, + imageQuality: options.imageOptions.imageQuality, + limit: options.limit, + ); + + if (paths.isEmpty) { + return []; + } + + return paths.map((dynamic path) => XFile(path as String)).toList(); + } + @override Future> getMedia({ required MediaOptions options, }) async { return (await _hostApi.pickMedia( _mediaOptionsToMediaSelectionOptions(options), - GeneralOptions( - allowMultiple: options.allowMultiple, - usePhotoPicker: useAndroidPhotoPicker, - ), + _mediaOptionsToGeneralOptions(options), )) .map((String? path) => XFile(path!)) .toList(); @@ -243,6 +266,7 @@ class ImagePickerAndroid extends ImagePickerPlatform { final ImageSelectionOptions imageSelectionOptions = _imageOptionsToImageSelectionOptionsWithValidator( mediaOptions.imageOptions); + return MediaSelectionOptions( imageSelectionOptions: imageSelectionOptions, ); @@ -270,6 +294,29 @@ class ImagePickerAndroid extends ImagePickerPlatform { quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth); } + GeneralOptions _mediaOptionsToGeneralOptions(MediaOptions options) { + final bool allowMultiple = options.allowMultiple; + final int? limit = options.limit; + + if (!allowMultiple && limit != null) { + throw ArgumentError.value( + allowMultiple, + 'allowMultiple', + 'cannot be false, when limit is not null', + ); + } + + if (limit != null && limit < 2) { + throw ArgumentError.value(limit, 'limit', 'cannot be lower then 2'); + } + + return GeneralOptions( + allowMultiple: allowMultiple, + usePhotoPicker: useAndroidPhotoPicker, + limit: limit, + ); + } + @override Future retrieveLostData() async { final LostDataResponse result = await getLostData(); diff --git a/packages/image_picker/image_picker_android/lib/src/messages.g.dart b/packages/image_picker/image_picker_android/lib/src/messages.g.dart index 476e80db001..1466b15aee8 100644 --- a/packages/image_picker/image_picker_android/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_android/lib/src/messages.g.dart @@ -1,9 +1,9 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// Autogenerated from Pigeon (v17.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -11,6 +11,24 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +List wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + enum SourceCamera { rear, front, @@ -30,16 +48,20 @@ class GeneralOptions { GeneralOptions({ required this.allowMultiple, required this.usePhotoPicker, + this.limit, }); bool allowMultiple; bool usePhotoPicker; + int? limit; + Object encode() { return [ allowMultiple, usePhotoPicker, + limit, ]; } @@ -48,6 +70,7 @@ class GeneralOptions { return GeneralOptions( allowMultiple: result[0]! as bool, usePhotoPicker: result[1]! as bool, + limit: result[2] as int?, ); } } @@ -195,7 +218,7 @@ class CacheRetrievalResult { CacheRetrievalResult({ required this.type, this.error, - required this.paths, + this.paths = const [], }); /// The type of the retrieved data. @@ -288,43 +311,43 @@ class ImagePickerApi { /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. ImagePickerApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; + : __pigeon_binaryMessenger = binaryMessenger; + final BinaryMessenger? __pigeon_binaryMessenger; - static const MessageCodec codec = _ImagePickerApiCodec(); + static const MessageCodec pigeonChannelCodec = + _ImagePickerApiCodec(); /// Selects images and returns their paths. /// /// Elements must not be null, by convention. See /// https://github.com/flutter/flutter/issues/97848 - Future> pickImages( - SourceSpecification arg_source, - ImageSelectionOptions arg_options, - GeneralOptions arg_generalOptions) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickImages', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_source, arg_options, arg_generalOptions]) - as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + Future> pickImages(SourceSpecification source, + ImageSelectionOptions options, GeneralOptions generalOptions) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickImages'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([source, options, generalOptions]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); - } else if (replyList[0] == null) { + } else if (__pigeon_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } @@ -332,34 +355,33 @@ class ImagePickerApi { /// /// Elements must not be null, by convention. See /// https://github.com/flutter/flutter/issues/97848 - Future> pickVideos( - SourceSpecification arg_source, - VideoSelectionOptions arg_options, - GeneralOptions arg_generalOptions) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickVideos', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_source, arg_options, arg_generalOptions]) - as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + Future> pickVideos(SourceSpecification source, + VideoSelectionOptions options, GeneralOptions generalOptions) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickVideos'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([source, options, generalOptions]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); - } else if (replyList[0] == null) { + } else if (__pigeon_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } @@ -367,55 +389,59 @@ class ImagePickerApi { /// /// Elements must not be null, by convention. See /// https://github.com/flutter/flutter/issues/97848 - Future> pickMedia( - MediaSelectionOptions arg_mediaSelectionOptions, - GeneralOptions arg_generalOptions) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_mediaSelectionOptions, arg_generalOptions]) + Future> pickMedia(MediaSelectionOptions mediaSelectionOptions, + GeneralOptions generalOptions) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickMedia'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([mediaSelectionOptions, generalOptions]) as List?; - if (replyList == null) { + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); - } else if (replyList.length > 1) { - throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], - ); - } else if (replyList[0] == null) { + } else if (__pigeon_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } /// Returns results from a previous app session, if any. Future retrieveLostResults() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.retrieveLostResults', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + const String __pigeon_channelName = + 'dev.flutter.pigeon.image_picker_android.ImagePickerApi.retrieveLostResults'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); } else { - return (replyList[0] as CacheRetrievalResult?); + return (__pigeon_replyList[0] as CacheRetrievalResult?); } } } diff --git a/packages/image_picker/image_picker_android/pigeons/messages.dart b/packages/image_picker/image_picker_android/pigeons/messages.dart index 9d264b5a11f..7f39ae501b6 100644 --- a/packages/image_picker/image_picker_android/pigeons/messages.dart +++ b/packages/image_picker/image_picker_android/pigeons/messages.dart @@ -14,9 +14,10 @@ import 'package:pigeon/pigeon.dart'; copyrightHeader: 'pigeons/copyright.txt', )) class GeneralOptions { - GeneralOptions(this.allowMultiple, this.usePhotoPicker); + GeneralOptions(this.allowMultiple, this.usePhotoPicker, this.limit); bool allowMultiple; bool usePhotoPicker; + int? limit; } /// Options for image selection and output. diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 7a98a4f221f..8f0606a558e 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_android description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.9+6 +version: 0.8.10 environment: - sdk: ^3.2.0 - flutter: ">=3.16.0" + sdk: ^3.3.0 + flutter: ">=3.19.0" flutter: plugin: @@ -21,13 +21,13 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.10.0 dev_dependencies: flutter_test: sdk: flutter mockito: 5.4.4 - pigeon: ^9.2.5 + pigeon: ^17.0.0 topics: - camera diff --git a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart index 0b0cab4d6df..172b135ac0a 100644 --- a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart +++ b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart @@ -156,6 +156,7 @@ void main() { expect(api.passedImageOptions?.maxWidth, null); expect(api.passedImageOptions?.maxHeight, null); expect(api.passedImageOptions?.quality, 100); + expect(api.limit, null); }); test('passes image option arguments correctly', () async { @@ -465,6 +466,7 @@ void main() { expect(api.passedImageOptions?.maxWidth, null); expect(api.passedImageOptions?.maxHeight, null); expect(api.passedImageOptions?.quality, 100); + expect(api.limit, null); }); test('passes image option arguments correctly', () async { @@ -681,6 +683,7 @@ void main() { expect(api.passedImageOptions?.maxWidth, null); expect(api.passedImageOptions?.maxHeight, null); expect(api.passedImageOptions?.quality, 100); + expect(api.limit, null); }); test('passes image option arguments correctly', () async { @@ -692,11 +695,13 @@ void main() { maxHeight: 20.0, imageQuality: 70, ), + limit: 5, )); expect(api.passedImageOptions?.maxWidth, 10.0); expect(api.passedImageOptions?.maxHeight, 20.0); expect(api.passedImageOptions?.quality, 70); + expect(api.limit, 5); }); test('does not accept a negative width or height argument', () { @@ -743,6 +748,37 @@ void main() { ); }); + test('does not accept an invalid limit argument', () { + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + limit: -1, + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + limit: 0, + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept a not null limit when allowMultiple is false', () { + expect( + () => picker.getMedia( + options: const MediaOptions(allowMultiple: false, limit: 5), + ), + throwsArgumentError, + ); + }); + test('handles an empty path response gracefully', () async { api.returnValue = []; @@ -926,6 +962,7 @@ class _FakeImagePickerApi implements ImagePickerApi { VideoSelectionOptions? passedVideoOptions; bool? passedAllowMultiple; bool? passedPhotoPickerFlag; + int? limit; _LastPickType? lastCall; @override @@ -939,6 +976,7 @@ class _FakeImagePickerApi implements ImagePickerApi { passedImageOptions = options; passedAllowMultiple = generalOptions.allowMultiple; passedPhotoPickerFlag = generalOptions.usePhotoPicker; + limit = generalOptions.limit; return returnValue as List? ?? []; } @@ -951,6 +989,7 @@ class _FakeImagePickerApi implements ImagePickerApi { passedImageOptions = options.imageSelectionOptions; passedPhotoPickerFlag = generalOptions.usePhotoPicker; passedAllowMultiple = generalOptions.allowMultiple; + limit = generalOptions.limit; return returnValue as List? ?? []; } diff --git a/packages/image_picker/image_picker_android/test/test_api.g.dart b/packages/image_picker/image_picker_android/test/test_api.g.dart index d3b68913a68..52aefcd0faa 100644 --- a/packages/image_picker/image_picker_android/test/test_api.g.dart +++ b/packages/image_picker/image_picker_android/test/test_api.g.dart @@ -1,9 +1,9 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// Autogenerated from Pigeon (v17.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -69,7 +69,8 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { abstract class TestHostImagePickerApi { static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => TestDefaultBinaryMessengerBinding.instance; - static const MessageCodec codec = _TestHostImagePickerApiCodec(); + static const MessageCodec pigeonChannelCodec = + _TestHostImagePickerApiCodec(); /// Selects images and returns their paths. /// @@ -98,111 +99,146 @@ abstract class TestHostImagePickerApi { static void setup(TestHostImagePickerApi? api, {BinaryMessenger? binaryMessenger}) { { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickImages', codec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickImages', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null.'); + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickImages was null.'); final List args = (message as List?)!; final SourceSpecification? arg_source = (args[0] as SourceSpecification?); assert(arg_source != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null SourceSpecification.'); + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickImages was null, expected non-null SourceSpecification.'); final ImageSelectionOptions? arg_options = (args[1] as ImageSelectionOptions?); assert(arg_options != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.'); + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.'); final GeneralOptions? arg_generalOptions = (args[2] as GeneralOptions?); assert(arg_generalOptions != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.'); - final List output = await api.pickImages( - arg_source!, arg_options!, arg_generalOptions!); - return [output]; + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.'); + try { + final List output = await api.pickImages( + arg_source!, arg_options!, arg_generalOptions!); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } }); } } { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickVideos', codec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickVideos', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null.'); + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickVideos was null.'); final List args = (message as List?)!; final SourceSpecification? arg_source = (args[0] as SourceSpecification?); assert(arg_source != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null SourceSpecification.'); + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickVideos was null, expected non-null SourceSpecification.'); final VideoSelectionOptions? arg_options = (args[1] as VideoSelectionOptions?); assert(arg_options != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null VideoSelectionOptions.'); + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickVideos was null, expected non-null VideoSelectionOptions.'); final GeneralOptions? arg_generalOptions = (args[2] as GeneralOptions?); assert(arg_generalOptions != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.'); - final List output = await api.pickVideos( - arg_source!, arg_options!, arg_generalOptions!); - return [output]; + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.'); + try { + final List output = await api.pickVideos( + arg_source!, arg_options!, arg_generalOptions!); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } }); } } { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickMedia', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.'); + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickMedia was null.'); final List args = (message as List?)!; final MediaSelectionOptions? arg_mediaSelectionOptions = (args[0] as MediaSelectionOptions?); assert(arg_mediaSelectionOptions != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); final GeneralOptions? arg_generalOptions = (args[1] as GeneralOptions?); assert(arg_generalOptions != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.'); - final List output = await api.pickMedia( - arg_mediaSelectionOptions!, arg_generalOptions!); - return [output]; + 'Argument for dev.flutter.pigeon.image_picker_android.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.'); + try { + final List output = await api.pickMedia( + arg_mediaSelectionOptions!, arg_generalOptions!); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } }); } } { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.retrieveLostResults', codec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.image_picker_android.ImagePickerApi.retrieveLostResults', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { - // ignore message - final CacheRetrievalResult? output = api.retrieveLostResults(); - return [output]; + try { + final CacheRetrievalResult? output = api.retrieveLostResults(); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } }); } } diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index 566aab0f686..787524c0e55 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.10 + +* Adds limit parameter to `MediaOptions` and `MultiImagePickerOptions` that sets a limit to how many media or image items can be selected. + ## 0.8.9+2 * Updates minimum iOS version to 12.0 and minimum Flutter version to 3.16.6. diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m index 36ca8ec9658..d54d5c3ec24 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -175,6 +175,7 @@ - (void)testPickMultiImageShouldUseUIImagePickerControllerOnPreiOS14 { [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] quality:@(50) fullMetadata:YES + limit:nil completion:^(NSArray *_Nullable result, FlutterError *_Nullable error){ }]; @@ -198,7 +199,8 @@ - (void)testPickMediaShouldUseUIImagePickerControllerOnPreiOS14 { [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] imageQuality:@(50) requestFullMetadata:YES - allowMultiple:YES]; + allowMultiple:YES + limit:nil]; [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions completion:^(NSArray *_Nullable result, @@ -236,6 +238,7 @@ - (void)testPickMultiImageWithoutFullMetadata { [plugin pickMultiImageWithMaxSize:[[FLTMaxSize alloc] init] quality:nil fullMetadata:NO + limit:nil completion:^(NSArray *_Nullable result, FlutterError *_Nullable error){ }]; @@ -254,7 +257,8 @@ - (void)testPickMediaWithoutFullMetadata { [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] imageQuality:@(50) requestFullMetadata:YES - allowMultiple:YES]; + allowMultiple:YES + limit:nil]; [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions @@ -544,6 +548,7 @@ - (void)testPickMultiImageDuplicateCallCancels API_AVAILABLE(ios(14)) { [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@100 height:@100] quality:nil fullMetadata:YES + limit:nil completion:^(NSArray *result, FlutterError *error) { XCTAssertNotNil(error); XCTAssertEqualObjects(error.code, @"multiple_request"); @@ -552,6 +557,7 @@ - (void)testPickMultiImageDuplicateCallCancels API_AVAILABLE(ios(14)) { [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@100 height:@100] quality:nil fullMetadata:YES + limit:nil completion:^(NSArray *result, FlutterError *error){ }]; [self waitForExpectationsWithTimeout:30 handler:nil]; @@ -570,7 +576,8 @@ - (void)testPickMediaDuplicateCallCancels API_AVAILABLE(ios(14)) { [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] imageQuality:@(50) requestFullMetadata:YES - allowMultiple:YES]; + allowMultiple:YES + limit:nil]; XCTestExpectation *firstCallExpectation = [self expectationWithDescription:@"first call"]; [plugin pickMediaWithMediaSelectionOptions:options completion:^(NSArray *result, FlutterError *error) { @@ -608,4 +615,79 @@ - (void)testPickVideoDuplicateCallCancels API_AVAILABLE(ios(14)) { [self waitForExpectationsWithTimeout:30 handler:nil]; } +- (void)testPickMultiImageWithLimit { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin pickMultiImageWithMaxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:NO + limit:@(2) + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + XCTAssertEqual(plugin.callContext.maxImageCount, 2); +} + +- (void)testPickMediaWithLimitAllowsMultiple { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:nil + requestFullMetadata:NO + allowMultiple:YES + limit:@(2)]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(plugin.callContext.maxImageCount, 2); +} + +- (void)testPickMediaWithLimitMultipleNotAllowed { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:nil + requestFullMetadata:NO + allowMultiple:NO + limit:@(2)]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(plugin.callContext.maxImageCount, 1); +} + +- (void)testPickMultiImageWithoutLimit { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin pickMultiImageWithMaxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:NO + limit:nil + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + XCTAssertEqual(plugin.callContext.maxImageCount, 0); +} + +- (void)testPickMediaWithoutLimitAllowsMultiple { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:nil + requestFullMetadata:NO + allowMultiple:YES + limit:nil]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(plugin.callContext.maxImageCount, 0); +} + @end diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart index 0f42b58ad2d..28d0ea2540e 100755 --- a/packages/image_picker/image_picker_ios/example/lib/main.dart +++ b/packages/image_picker/image_picker_ios/example/lib/main.dart @@ -56,6 +56,7 @@ class _MyHomePageState extends State { final TextEditingController maxWidthController = TextEditingController(); final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); + final TextEditingController limitController = TextEditingController(); Future _playVideo(XFile? file) async { if (file != null && mounted) { @@ -88,18 +89,20 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, true, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final List pickedFileList = isMedia ? await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - )), + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + limit: limit, + ), ) : await _picker.getMultiImageWithOptions( options: MultiImagePickerOptions( @@ -108,6 +111,7 @@ class _MyHomePageState extends State { maxHeight: maxHeight, imageQuality: quality, ), + limit: limit, ), ); setState(() { @@ -120,18 +124,19 @@ class _MyHomePageState extends State { } }); } else if (isMedia) { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, false, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final List pickedFileList = []; final XFile? media = _firstOrNull(await _picker.getMedia( options: MediaOptions( - allowMultiple: isMultiImage, - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - )), + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), )); if (media != null) { @@ -145,8 +150,8 @@ class _MyHomePageState extends State { } }); } else { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context, false, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { try { final XFile? pickedFile = await _picker.getImageFromSource( source: source, @@ -400,7 +405,7 @@ class _MyHomePageState extends State { } Future _displayPickImageDialog( - BuildContext context, OnPickImageCallback onPick) async { + BuildContext context, bool isMulti, OnPickImageCallback onPick) async { return showDialog( context: context, builder: (BuildContext context) { @@ -428,6 +433,13 @@ class _MyHomePageState extends State { decoration: const InputDecoration( hintText: 'Enter quality if desired'), ), + if (isMulti) + TextField( + controller: limitController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter limit if desired'), + ), ], ), actions: [ @@ -449,7 +461,10 @@ class _MyHomePageState extends State { final int? quality = qualityController.text.isNotEmpty ? int.parse(qualityController.text) : null; - onPick(width, height, quality); + final int? limit = limitController.text.isNotEmpty + ? int.parse(limitController.text) + : null; + onPick(width, height, quality, limit); Navigator.of(context).pop(); }), ], @@ -459,7 +474,7 @@ class _MyHomePageState extends State { } typedef OnPickImageCallback = void Function( - double? maxWidth, double? maxHeight, int? quality); + double? maxWidth, double? maxHeight, int? quality, int? limit); class AspectRatioVideo extends StatefulWidget { const AspectRatioVideo(this.controller, {super.key}); diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml index e4dd0908f96..57c9e2d83fc 100755 --- a/packages/image_picker/image_picker_ios/example/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the image_picker plugin. publish_to: none environment: - sdk: ^3.2.3 - flutter: ">=3.16.6" + sdk: ^3.3.0 + flutter: ">=3.19.0" dependencies: flutter: @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.10.0 mime: ^1.0.4 video_player: ^2.1.4 diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m index 10081a8c13e..c65542ddf12 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -190,6 +190,7 @@ - (void)pickImageWithSource:(nonnull FLTSourceSpecification *)source - (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize quality:(nullable NSNumber *)imageQuality fullMetadata:(BOOL)fullMetadata + limit:(nullable NSNumber *)limit completion:(nonnull void (^)(NSArray *_Nullable, FlutterError *_Nullable))completion { [self cancelInProgressCall]; @@ -198,6 +199,7 @@ - (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize context.maxSize = maxSize; context.imageQuality = imageQuality; context.requestFullMetadata = fullMetadata; + context.maxImageCount = limit.intValue; if (@available(iOS 14, *)) { [self launchPHPickerWithContext:context]; @@ -219,8 +221,11 @@ - (void)pickMediaWithMediaSelectionOptions:(nonnull FLTMediaSelectionOptions *)m context.imageQuality = [mediaSelectionOptions imageQuality]; context.requestFullMetadata = [mediaSelectionOptions requestFullMetadata]; context.includeVideo = YES; + NSNumber *limit = [mediaSelectionOptions limit]; if (!mediaSelectionOptions.allowMultiple) { context.maxImageCount = 1; + } else if (limit != nil) { + context.maxImageCount = limit.intValue; } if (@available(iOS 14, *)) { diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h index 593f88296b8..de9a8200b82 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// Autogenerated from Pigeon (v17.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -51,11 +51,13 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { + (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize imageQuality:(nullable NSNumber *)imageQuality requestFullMetadata:(BOOL)requestFullMetadata - allowMultiple:(BOOL)allowMultiple; + allowMultiple:(BOOL)allowMultiple + limit:(nullable NSNumber *)limit; @property(nonatomic, strong) FLTMaxSize *maxSize; @property(nonatomic, strong, nullable) NSNumber *imageQuality; @property(nonatomic, assign) BOOL requestFullMetadata; @property(nonatomic, assign) BOOL allowMultiple; +@property(nonatomic, strong, nullable) NSNumber *limit; @end @interface FLTSourceSpecification : NSObject @@ -78,6 +80,7 @@ NSObject *FLTImagePickerApiGetCodec(void); - (void)pickMultiImageWithMaxSize:(FLTMaxSize *)maxSize quality:(nullable NSNumber *)imageQuality fullMetadata:(BOOL)requestFullMetadata + limit:(nullable NSNumber *)limit completion:(void (^)(NSArray *_Nullable, FlutterError *_Nullable))completion; - (void)pickVideoWithSource:(FLTSourceSpecification *)source diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m index 1659cb88703..a1f5794dd6a 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// Autogenerated from Pigeon (v17.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" @@ -16,6 +16,20 @@ #error File requires ARC to be enabled. #endif +static NSArray *wrapResult(id result, FlutterError *error) { + if (error) { + return @[ + error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] + ]; + } + return @[ result ?: [NSNull null] ]; +} + +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + @implementation FLTSourceCameraBox - (instancetype)initWithValue:(FLTSourceCamera)value { self = [super init]; @@ -36,19 +50,6 @@ - (instancetype)initWithValue:(FLTSourceType)value { } @end -static NSArray *wrapResult(id result, FlutterError *error) { - if (error) { - return @[ - error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] - ]; - } - return @[ result ?: [NSNull null] ]; -} -static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { - id result = array[key]; - return (result == [NSNull null]) ? nil : result; -} - @interface FLTMaxSize () + (FLTMaxSize *)fromList:(NSArray *)list; + (nullable FLTMaxSize *)nullableFromList:(NSArray *)list; @@ -95,12 +96,14 @@ @implementation FLTMediaSelectionOptions + (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize imageQuality:(nullable NSNumber *)imageQuality requestFullMetadata:(BOOL)requestFullMetadata - allowMultiple:(BOOL)allowMultiple { + allowMultiple:(BOOL)allowMultiple + limit:(nullable NSNumber *)limit { FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init]; pigeonResult.maxSize = maxSize; pigeonResult.imageQuality = imageQuality; pigeonResult.requestFullMetadata = requestFullMetadata; pigeonResult.allowMultiple = allowMultiple; + pigeonResult.limit = limit; return pigeonResult; } + (FLTMediaSelectionOptions *)fromList:(NSArray *)list { @@ -109,6 +112,7 @@ + (FLTMediaSelectionOptions *)fromList:(NSArray *)list { pigeonResult.imageQuality = GetNullableObjectAtIndex(list, 1); pigeonResult.requestFullMetadata = [GetNullableObjectAtIndex(list, 2) boolValue]; pigeonResult.allowMultiple = [GetNullableObjectAtIndex(list, 3) boolValue]; + pigeonResult.limit = GetNullableObjectAtIndex(list, 4); return pigeonResult; } + (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list { @@ -120,6 +124,7 @@ - (NSArray *)toList { self.imageQuality ?: [NSNull null], @(self.requestFullMetadata), @(self.allowMultiple), + self.limit ?: [NSNull null], ]; } @end @@ -244,18 +249,20 @@ void SetUpFLTImagePickerApi(id binaryMessenger, codec:FLTImagePickerApiGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector - (pickMultiImageWithMaxSize:quality:fullMetadata:completion:)], + (pickMultiImageWithMaxSize:quality:fullMetadata:limit:completion:)], @"FLTImagePickerApi api (%@) doesn't respond to " - @"@selector(pickMultiImageWithMaxSize:quality:fullMetadata:completion:)", + @"@selector(pickMultiImageWithMaxSize:quality:fullMetadata:limit:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 0); NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 1); BOOL arg_requestFullMetadata = [GetNullableObjectAtIndex(args, 2) boolValue]; + NSNumber *arg_limit = GetNullableObjectAtIndex(args, 3); [api pickMultiImageWithMaxSize:arg_maxSize quality:arg_imageQuality fullMetadata:arg_requestFullMetadata + limit:arg_limit completion:^(NSArray *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart index 4c302cae5f1..96166130797 100644 --- a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -133,12 +133,19 @@ class ImagePickerIOS extends ImagePickerPlatform { throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); } + final int? limit = options.limit; + if (limit != null && limit < 2) { + throw ArgumentError.value(limit, 'limit', 'cannot be lower than 2'); + } + // TODO(stuartmorgan): Remove the cast once Pigeon supports non-nullable // generics, https://github.com/flutter/flutter/issues/97848 return (await _hostApi.pickMultiImage( - MaxSize(width: maxWidth, height: maxHeight), - imageQuality, - options.imageOptions.requestFullMetadata)) + MaxSize(width: maxWidth, height: maxHeight), + imageQuality, + options.imageOptions.requestFullMetadata, + limit, + )) .cast(); } @@ -210,11 +217,28 @@ class ImagePickerIOS extends ImagePickerPlatform { MediaOptions mediaOptions) { final MaxSize maxSize = _imageOptionsToMaxSizeWithValidation(mediaOptions.imageOptions); + + final bool allowMultiple = mediaOptions.allowMultiple; + final int? limit = mediaOptions.limit; + + if (!allowMultiple && limit != null) { + throw ArgumentError.value( + allowMultiple, + 'allowMultiple', + 'cannot be false, when limit is not null', + ); + } + + if (limit != null && limit < 2) { + throw ArgumentError.value(limit, 'limit', 'cannot be lower than 2'); + } + return MediaSelectionOptions( maxSize: maxSize, imageQuality: mediaOptions.imageOptions.imageQuality, requestFullMetadata: mediaOptions.imageOptions.requestFullMetadata, allowMultiple: mediaOptions.allowMultiple, + limit: mediaOptions.limit, ); } diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart index 0ab7d1b5186..b3e785a09df 100644 --- a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -1,9 +1,9 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// Autogenerated from Pigeon (v17.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -11,6 +11,13 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + List wrapResponse( {Object? result, PlatformException? error, bool empty = false}) { if (empty) { @@ -64,6 +71,7 @@ class MediaSelectionOptions { this.imageQuality, required this.requestFullMetadata, required this.allowMultiple, + this.limit, }); MaxSize maxSize; @@ -74,12 +82,15 @@ class MediaSelectionOptions { bool allowMultiple; + int? limit; + Object encode() { return [ maxSize.encode(), imageQuality, requestFullMetadata, allowMultiple, + limit, ]; } @@ -90,6 +101,7 @@ class MediaSelectionOptions { imageQuality: result[1] as int?, requestFullMetadata: result[2]! as bool, allowMultiple: result[3]! as bool, + limit: result[4] as int?, ); } } @@ -158,117 +170,122 @@ class ImagePickerApi { /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. ImagePickerApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _ImagePickerApiCodec(); - - Future pickImage(SourceSpecification arg_source, MaxSize arg_maxSize, - int? arg_imageQuality, bool arg_requestFullMetadata) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickImage', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send([ - arg_source, - arg_maxSize, - arg_imageQuality, - arg_requestFullMetadata - ]) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + : __pigeon_binaryMessenger = binaryMessenger; + final BinaryMessenger? __pigeon_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = + _ImagePickerApiCodec(); + + Future pickImage(SourceSpecification source, MaxSize maxSize, + int? imageQuality, bool requestFullMetadata) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickImage'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([source, maxSize, imageQuality, requestFullMetadata]) + as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); } else { - return (replyList[0] as String?); + return (__pigeon_replyList[0] as String?); } } - Future> pickMultiImage(MaxSize arg_maxSize, - int? arg_imageQuality, bool arg_requestFullMetadata) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMultiImage', - codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send( - [arg_maxSize, arg_imageQuality, arg_requestFullMetadata]) + Future> pickMultiImage(MaxSize maxSize, int? imageQuality, + bool requestFullMetadata, int? limit) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMultiImage'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([maxSize, imageQuality, requestFullMetadata, limit]) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); - } else if (replyList[0] == null) { + } else if (__pigeon_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } Future pickVideo( - SourceSpecification arg_source, int? arg_maxDurationSeconds) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickVideo', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_source, arg_maxDurationSeconds]) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + SourceSpecification source, int? maxDurationSeconds) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickVideo'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([source, maxDurationSeconds]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); } else { - return (replyList[0] as String?); + return (__pigeon_replyList[0] as String?); } } /// Selects images and videos and returns their paths. Future> pickMedia( - MediaSelectionOptions arg_mediaSelectionOptions) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMedia', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_mediaSelectionOptions]) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + MediaSelectionOptions mediaSelectionOptions) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMedia'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = await __pigeon_channel + .send([mediaSelectionOptions]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); - } else if (replyList[0] == null) { + } else if (__pigeon_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } } diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart index 0e69598304f..d8ae8954e98 100644 --- a/packages/image_picker/image_picker_ios/pigeons/messages.dart +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -26,12 +26,14 @@ class MediaSelectionOptions { this.imageQuality, required this.requestFullMetadata, required this.allowMultiple, + this.limit, }); MaxSize maxSize; int? imageQuality; bool requestFullMetadata; bool allowMultiple; + int? limit; } // Corresponds to `CameraDevice` from the platform interface package. @@ -53,9 +55,9 @@ abstract class ImagePickerApi { String? pickImage(SourceSpecification source, MaxSize maxSize, int? imageQuality, bool requestFullMetadata); @async - @ObjCSelector('pickMultiImageWithMaxSize:quality:fullMetadata:') + @ObjCSelector('pickMultiImageWithMaxSize:quality:fullMetadata:limit:') List pickMultiImage( - MaxSize maxSize, int? imageQuality, bool requestFullMetadata); + MaxSize maxSize, int? imageQuality, bool requestFullMetadata, int? limit); @async @ObjCSelector('pickVideoWithSource:maxDuration:') String? pickVideo(SourceSpecification source, int? maxDurationSeconds); diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index c1b3c8d6221..70d578be360 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_ios description: iOS implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.9+2 +version: 0.8.10 environment: - sdk: ^3.2.3 - flutter: ">=3.16.6" + sdk: ^3.3.0 + flutter: ">=3.19.0" flutter: plugin: @@ -19,16 +19,16 @@ flutter: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.8.0 + image_picker_platform_interface: ^2.10.0 dev_dependencies: flutter_test: sdk: flutter mockito: 5.4.4 - pigeon: ^13.0.0 + pigeon: ^17.0.0 topics: - camera - image-picker - files - - file-selection + - file-selection \ No newline at end of file diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart index 77fa322de6c..c2e5f74763f 100644 --- a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -61,12 +61,14 @@ class _ApiLogger implements TestHostImagePickerApi { MaxSize maxSize, int? imageQuality, bool requestFullMetadata, + int? limit, ) async { calls.add(_LoggedMethodCall('pickMultiImage', arguments: { 'maxWidth': maxSize.width, 'maxHeight': maxSize.height, 'imageQuality': imageQuality, 'requestFullMetadata': requestFullMetadata, + 'limit': limit, })); return returnValue as List; } @@ -80,6 +82,7 @@ class _ApiLogger implements TestHostImagePickerApi { 'imageQuality': mediaSelectionOptions.imageQuality, 'requestFullMetadata': mediaSelectionOptions.requestFullMetadata, 'allowMultiple': mediaSelectionOptions.allowMultiple, + 'limit': mediaSelectionOptions.limit, })); return returnValue as List; } @@ -329,6 +332,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -370,6 +374,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -377,6 +382,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -384,6 +390,7 @@ void main() { 'maxHeight': 10.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -391,6 +398,7 @@ void main() { 'maxHeight': 20.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -398,6 +406,7 @@ void main() { 'maxHeight': null, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -405,6 +414,7 @@ void main() { 'maxHeight': 10.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -412,6 +422,7 @@ void main() { 'maxHeight': 20.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -769,6 +780,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -810,6 +822,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -817,6 +830,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -824,6 +838,7 @@ void main() { 'maxHeight': 10.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -831,6 +846,7 @@ void main() { 'maxHeight': 20.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -838,6 +854,7 @@ void main() { 'maxHeight': null, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -845,6 +862,7 @@ void main() { 'maxHeight': 10.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -852,6 +870,7 @@ void main() { 'maxHeight': 20.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -903,7 +922,8 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, }), ], ); @@ -959,6 +979,16 @@ void main() { imageQuality: 70, ), )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + limit: 5, + )); expect( log.calls, @@ -968,49 +998,64 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, }), const _LoggedMethodCall('pickMedia', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, }), const _LoggedMethodCall('pickMedia', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, 'requestFullMetadata': true, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, }), const _LoggedMethodCall('pickMedia', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, 'requestFullMetadata': true, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, }), const _LoggedMethodCall('pickMedia', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, 'requestFullMetadata': true, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, }), const _LoggedMethodCall('pickMedia', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, 'requestFullMetadata': true, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, }), const _LoggedMethodCall('pickMedia', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, 'requestFullMetadata': true, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true, + 'limit': 5, }), ], ); @@ -1032,7 +1077,8 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': false, - 'allowMultiple': true + 'allowMultiple': true, + 'limit': null, }), ], ); @@ -1053,7 +1099,8 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, - 'allowMultiple': false + 'allowMultiple': false, + 'limit': null, }), ], ); @@ -1101,6 +1148,36 @@ void main() { ); }); + test('does not accept a invalid limit argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + limit: -1, + )), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + limit: 0, + )), + throwsArgumentError, + ); + }); + + test('does not accept a not null limit when allowMultiple is false', () { + expect( + () => picker.getMedia( + options: const MediaOptions(allowMultiple: false, limit: 5), + ), + throwsArgumentError, + ); + }); + test('handles a empty path response gracefully', () async { log.returnValue = []; @@ -1501,6 +1578,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -1543,6 +1621,16 @@ void main() { ), ), ); + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + limit: 5, + ), + ); expect( log.calls, @@ -1553,6 +1641,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -1560,6 +1649,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -1567,6 +1657,7 @@ void main() { 'maxHeight': 10.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -1574,6 +1665,7 @@ void main() { 'maxHeight': 20.0, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -1581,6 +1673,7 @@ void main() { 'maxHeight': null, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -1588,6 +1681,15 @@ void main() { 'maxHeight': 10.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'limit': null, }), const _LoggedMethodCall('pickMultiImage', arguments: { @@ -1595,6 +1697,7 @@ void main() { 'maxHeight': 20.0, 'imageQuality': 70, 'requestFullMetadata': true, + 'limit': 5, }), ], ); @@ -1642,6 +1745,27 @@ void main() { ); }); + test('does not accept a invalid limit argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + limit: -1, + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + limit: 0, + ), + ), + throwsArgumentError, + ); + }); + test('handles an empty response', () async { log.returnValue = []; @@ -1661,6 +1785,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': true, + 'limit': null, }), ], ); @@ -1683,6 +1808,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'requestFullMetadata': false, + 'limit': null, }), ], ); diff --git a/packages/image_picker/image_picker_ios/test/test_api.g.dart b/packages/image_picker/image_picker_ios/test/test_api.g.dart index a2d02666baa..4208c68e32f 100644 --- a/packages/image_picker/image_picker_ios/test/test_api.g.dart +++ b/packages/image_picker/image_picker_ios/test/test_api.g.dart @@ -1,9 +1,9 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// Autogenerated from Pigeon (v17.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -49,13 +49,14 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { abstract class TestHostImagePickerApi { static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => TestDefaultBinaryMessengerBinding.instance; - static const MessageCodec codec = _TestHostImagePickerApiCodec(); + static const MessageCodec pigeonChannelCodec = + _TestHostImagePickerApiCodec(); Future pickImage(SourceSpecification source, MaxSize maxSize, int? imageQuality, bool requestFullMetadata); Future> pickMultiImage( - MaxSize maxSize, int? imageQuality, bool requestFullMetadata); + MaxSize maxSize, int? imageQuality, bool requestFullMetadata, int? limit); Future pickVideo( SourceSpecification source, int? maxDurationSeconds); @@ -66,15 +67,17 @@ abstract class TestHostImagePickerApi { static void setup(TestHostImagePickerApi? api, {BinaryMessenger? binaryMessenger}) { { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickImage', codec, - binaryMessenger: binaryMessenger); + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickImage', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickImage was null.'); @@ -104,16 +107,17 @@ abstract class TestHostImagePickerApi { } } { - final BasicMessageChannel channel = BasicMessageChannel( + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMultiImage', - codec, + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMultiImage was null.'); @@ -125,9 +129,10 @@ abstract class TestHostImagePickerApi { final bool? arg_requestFullMetadata = (args[2] as bool?); assert(arg_requestFullMetadata != null, 'Argument for dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMultiImage was null, expected non-null bool.'); + final int? arg_limit = (args[3] as int?); try { - final List output = await api.pickMultiImage( - arg_maxSize!, arg_imageQuality, arg_requestFullMetadata!); + final List output = await api.pickMultiImage(arg_maxSize!, + arg_imageQuality, arg_requestFullMetadata!, arg_limit); return [output]; } on PlatformException catch (e) { return wrapResponse(error: e); @@ -139,15 +144,17 @@ abstract class TestHostImagePickerApi { } } { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickVideo', codec, - binaryMessenger: binaryMessenger); + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickVideo', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickVideo was null.'); @@ -171,15 +178,17 @@ abstract class TestHostImagePickerApi { } } { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMedia', codec, - binaryMessenger: binaryMessenger); + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + 'dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMedia', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.image_picker_ios.ImagePickerApi.pickMedia was null.');