From fb5c86c9532ba883e9e5bf7598e64196c3e85554 Mon Sep 17 00:00:00 2001 From: Piotr Mitkowski Date: Wed, 5 Oct 2022 19:01:05 +0200 Subject: [PATCH] [image_picker] add requestFullMetadata for iOS (optional permissions) (#5915) --- .../image_picker/image_picker/CHANGELOG.md | 3 +- packages/image_picker/image_picker/README.md | 1 + .../image_picker/example/lib/main.dart | 2 +- .../image_picker/lib/image_picker.dart | 70 ++-- .../image_picker/image_picker/pubspec.yaml | 6 +- .../image_picker/test/image_picker_test.dart | 355 +++++++++++++++--- .../test/image_picker_test.mocks.dart | 18 + 7 files changed, 386 insertions(+), 69 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index ccf1cb3c5e7d..76192566b18b 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 0.8.6 * Updates minimum Flutter version to 2.10. * Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Adds `requestFullMetadata` option to `pickImage`, so images on iOS can be picked without `Photo Library Usage` permission. ## 0.8.5+3 diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 2fa20be34859..aadfc83ff5e6 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -23,6 +23,7 @@ As a result of implementing PHPicker it becomes impossible to pick HEIC images o Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: * `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor. + * This permission is not required for image picking on iOS 11+ if you pass `false` for `requestFullMetadata`. * `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor. * `NSMicrophoneUsageDescription` - describe why your app needs access to the microphone, if you intend to record videos. This is called _Privacy - Microphone Usage Description_ in the visual editor. diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 4eecc5fa2a1a..5e448ddbee68 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -93,7 +93,7 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context!, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List? pickedFileList = await _picker.pickMultiImage( + final List pickedFileList = await _picker.pickMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 84c649028c96..2e266ccd5a5a 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -173,8 +173,9 @@ class ImagePicker { /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. /// - /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used - /// in addition to a size modification, of which the usage is explained below. + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. /// /// If specified, the image will be at most `maxWidth` wide and /// `maxHeight` tall. Otherwise the image will be returned at it's @@ -182,14 +183,22 @@ class ImagePicker { /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 /// where 100 is the original/max quality. If `imageQuality` is null, the image with /// the original quality will be returned. Compression is only supported for certain - /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, - /// a warning message will be logged. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if - /// the front or rear camera should be opened, this function is not guaranteed - /// to work on an Android device. + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is + /// [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. + /// It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter + /// for an intent to specify if the front or rear camera should be opened, this + /// function is not guaranteed to work on an Android device. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. /// /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. @@ -206,6 +215,7 @@ class ImagePicker { double? maxHeight, int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( @@ -218,12 +228,15 @@ class ImagePicker { throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); } - return platform.getImage( + return platform.getImageFromSource( source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + requestFullMetadata: requestFullMetadata, + ), ); } @@ -239,11 +252,18 @@ class ImagePicker { /// If specified, the images will be at most `maxWidth` wide and /// `maxHeight` tall. Otherwise the images will be returned at it's /// original width and height. + /// /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 /// where 100 is the original/max quality. If `imageQuality` is null, the images with /// the original quality will be returned. Compression is only supported for certain - /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, - /// a warning message will be logged. + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. /// /// The method could throw [PlatformException] if the app does not have permission to access /// the camera or photos gallery, no camera is available, plugin is already in use, @@ -251,10 +271,11 @@ class ImagePicker { /// be allocated (Android only) or due to an unknown error. /// /// See also [pickImage] to allow users to only pick a single image. - Future?> pickMultiImage({ + Future> pickMultiImage({ double? maxWidth, double? maxHeight, int? imageQuality, + bool requestFullMetadata = true, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( @@ -267,10 +288,15 @@ class ImagePicker { throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); } - return platform.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, + return platform.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ), + ), ); } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 58f9f0658483..7fed3bf4637b 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ 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/plugins/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: 0.8.5+3 +version: 0.8.6 environment: sdk: ">=2.14.0 <3.0.0" @@ -24,8 +24,8 @@ dependencies: sdk: flutter image_picker_android: ^0.8.4+11 image_picker_for_web: ^2.1.0 - image_picker_ios: ^0.8.4+11 - image_picker_platform_interface: ^2.3.0 + image_picker_ios: ^0.8.6+1 + image_picker_platform_interface: ^2.6.1 dev_dependencies: build_runner: ^2.1.10 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 10b5ac16e570..2d959bd20f7b 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -29,12 +29,8 @@ void main() { group('#Single image/video', () { group('#pickImage', () { setUp(() { - when(mockPlatform.getImage( - source: anyNamed('source'), - maxWidth: anyNamed('maxWidth'), - maxHeight: anyNamed('maxHeight'), - imageQuality: anyNamed('imageQuality'), - preferredCameraDevice: anyNamed('preferredCameraDevice'))) + when(mockPlatform.getImageFromSource( + source: anyNamed('source'), options: anyNamed('options'))) .thenAnswer((Invocation _) async => null); }); @@ -44,8 +40,20 @@ void main() { await picker.pickImage(source: ImageSource.gallery); verifyInOrder([ - mockPlatform.getImage(source: ImageSource.camera), - mockPlatform.getImage(source: ImageSource.gallery), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.gallery, + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), ]); }); @@ -76,20 +84,111 @@ void main() { imageQuality: 70); verifyInOrder([ - mockPlatform.getImage(source: ImageSource.camera), - mockPlatform.getImage(source: ImageSource.camera, maxWidth: 10.0), - mockPlatform.getImage(source: ImageSource.camera, maxHeight: 10.0), - mockPlatform.getImage( - source: ImageSource.camera, maxWidth: 10.0, maxHeight: 20.0), - mockPlatform.getImage( - source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70), - mockPlatform.getImage( - source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70), - mockPlatform.getImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - imageQuality: 70), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', isNull) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', isNull) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + isNull), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', equals(10.0)) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', isNull) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + isNull), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', isNull) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', equals(10.0)) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + isNull), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', equals(10.0)) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', equals(20.0)) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + isNull), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', equals(10.0)) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', isNull) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', isNull) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', equals(10.0)) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf() + .having((ImagePickerOptions options) => options.maxWidth, + 'maxWidth', equals(10.0)) + .having((ImagePickerOptions options) => options.maxHeight, + 'maxHeight', equals(20.0)) + .having( + (ImagePickerOptions options) => options.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), ]); }); @@ -117,7 +216,16 @@ void main() { final ImagePicker picker = ImagePicker(); await picker.pickImage(source: ImageSource.camera); - verify(mockPlatform.getImage(source: ImageSource.camera)); + verify(mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf().having( + (ImagePickerOptions options) => options.preferredCameraDevice, + 'preferredCameraDevice', + equals(CameraDevice.rear)), + named: 'options', + ), + )); }); test('camera position can set to front', () async { @@ -126,9 +234,51 @@ void main() { source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); - verify(mockPlatform.getImage( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front)); + verify(mockPlatform.getImageFromSource( + source: ImageSource.camera, + options: argThat( + isInstanceOf().having( + (ImagePickerOptions options) => options.preferredCameraDevice, + 'preferredCameraDevice', + equals(CameraDevice.front)), + named: 'options', + ), + )); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickImage(source: ImageSource.gallery); + + verify(mockPlatform.getImageFromSource( + source: ImageSource.gallery, + options: argThat( + isInstanceOf().having( + (ImagePickerOptions options) => options.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickImage( + source: ImageSource.gallery, + requestFullMetadata: false, + ); + + verify(mockPlatform.getImageFromSource( + source: ImageSource.gallery, + options: argThat( + isInstanceOf().having( + (ImagePickerOptions options) => options.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); }); }); @@ -250,11 +400,11 @@ void main() { group('#Multi images', () { setUp(() { - when(mockPlatform.getMultiImage( - maxWidth: anyNamed('maxWidth'), - maxHeight: anyNamed('maxHeight'), - imageQuality: anyNamed('imageQuality'))) - .thenAnswer((Invocation _) async => null); + when( + mockPlatform.getMultiImageWithOptions( + options: anyNamed('options'), + ), + ).thenAnswer((Invocation _) async => []); }); group('#pickMultiImage', () { @@ -283,14 +433,101 @@ void main() { maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); verifyInOrder([ - mockPlatform.getMultiImage(), - mockPlatform.getMultiImage(maxWidth: 10.0), - mockPlatform.getMultiImage(maxHeight: 10.0), - mockPlatform.getMultiImage(maxWidth: 10.0, maxHeight: 20.0), - mockPlatform.getMultiImage(maxWidth: 10.0, imageQuality: 70), - mockPlatform.getMultiImage(maxHeight: 10.0, imageQuality: 70), - mockPlatform.getMultiImage( - maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf().having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf().having( + (MultiImagePickerOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf() + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(20.0)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf() + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf() + .having( + (MultiImagePickerOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)) + .having( + (MultiImagePickerOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + 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)), + named: 'options', + ), + ), ]); }); @@ -307,11 +544,45 @@ void main() { ); }); - test('handles a null image file response gracefully', () async { + test('handles an empty image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); + + expect(await picker.pickMultiImage(), isEmpty); + expect(await picker.pickMultiImage(), isEmpty); + }); + + test('full metadata argument defaults to true', () async { final ImagePicker picker = ImagePicker(); + await picker.pickMultiImage(); + + verify(mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf().having( + (MultiImagePickerOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultiImage( + requestFullMetadata: false, + ); - expect(await picker.pickMultiImage(), isNull); - expect(await picker.pickMultiImage(), isNull); + verify(mockPlatform.getMultiImageWithOptions( + options: argThat( + isInstanceOf().having( + (MultiImagePickerOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); }); }); }); diff --git a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart index a79d076dd42c..f749b538665b 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -51,6 +51,7 @@ class MockImagePickerPlatform extends _i1.Mock }), returnValue: Future<_i2.PickedFile?>.value()) as _i4.Future<_i2.PickedFile?>); + @override _i4.Future?> pickMultiImage( {double? maxWidth, double? maxHeight, int? imageQuality}) => @@ -62,6 +63,7 @@ class MockImagePickerPlatform extends _i1.Mock }), returnValue: Future?>.value()) as _i4.Future?>); + @override _i4.Future<_i2.PickedFile?> pickVideo( {_i2.ImageSource? source, @@ -75,11 +77,13 @@ class MockImagePickerPlatform extends _i1.Mock }), returnValue: Future<_i2.PickedFile?>.value()) as _i4.Future<_i2.PickedFile?>); + @override _i4.Future<_i2.LostData> retrieveLostData() => (super.noSuchMethod(Invocation.method(#retrieveLostData, []), returnValue: Future<_i2.LostData>.value(_FakeLostData_0())) as _i4.Future<_i2.LostData>); + @override _i4.Future<_i5.XFile?> getImage( {_i2.ImageSource? source, @@ -96,6 +100,7 @@ class MockImagePickerPlatform extends _i1.Mock #preferredCameraDevice: preferredCameraDevice }), returnValue: Future<_i5.XFile?>.value()) as _i4.Future<_i5.XFile?>); + @override _i4.Future?> getMultiImage( {double? maxWidth, double? maxHeight, int? imageQuality}) => @@ -107,6 +112,7 @@ class MockImagePickerPlatform extends _i1.Mock }), returnValue: Future?>.value()) as _i4.Future?>); + @override _i4.Future<_i5.XFile?> getVideo( {_i2.ImageSource? source, @@ -119,12 +125,14 @@ class MockImagePickerPlatform extends _i1.Mock #maxDuration: maxDuration }), returnValue: Future<_i5.XFile?>.value()) as _i4.Future<_i5.XFile?>); + @override _i4.Future<_i2.LostDataResponse> getLostData() => (super.noSuchMethod(Invocation.method(#getLostData, []), returnValue: Future<_i2.LostDataResponse>.value(_FakeLostDataResponse_1())) as _i4.Future<_i2.LostDataResponse>); + @override _i4.Future<_i5.XFile?> getImageFromSource( {_i2.ImageSource? source, @@ -133,4 +141,14 @@ class MockImagePickerPlatform extends _i1.Mock Invocation.method( #getImageFromSource, [], {#source: source, #options: options}), returnValue: Future<_i5.XFile?>.value()) as _i4.Future<_i5.XFile?>); + + @override + _i4.Future> getMultiImageWithOptions( + {_i2.MultiImagePickerOptions? options = + const _i2.MultiImagePickerOptions()}) => + (super.noSuchMethod( + Invocation.method(#getMultiImageWithOptions, [], {#options: options}), + returnValue: + Future>.value(<_i5.XFile>[])) as _i4 + .Future>); }