diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index d9127e24d2af..7147c0f6f7de 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.8.1 + +* Add a new method `getMultiImage` to allow picking multiple images on iOS 14 or higher +and Android 4.3 or higher. Returns only 1 image for lower versions of iOS and Android. +* Known issue: On Android, `getLostData` will only get the last picked image when picking multiple images, +see: [#84634](https://github.com/flutter/flutter/issues/84634). + ## 0.8.0+4 * Cleaned up the README example @@ -49,7 +56,7 @@ is not included selected photos and image is scaled. ## 0.7.3 -* Endorse image_picker_for_web +* Endorse image_picker_for_web. ## 0.7.2+1 @@ -57,7 +64,7 @@ is not included selected photos and image is scaled. ## 0.7.2 -* Run CocoaPods iOS tests in RunnerUITests target +* Run CocoaPods iOS tests in RunnerUITests target. ## 0.7.1 diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 10899e2d85fb..3b3746d9f63e 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -11,6 +11,9 @@ First, add `image_picker` as a [dependency in your pubspec.yaml file](https://fl ### iOS +Starting with version **0.8.1** the iOS implementation uses PHPicker to pick (multiple) images on iOS 14 or higher. +As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue.[63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) + 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. @@ -19,6 +22,8 @@ Add the following keys to your _Info.plist_ file, located in `/ios ### Android +Starting with version **0.8.1** the Android implementation support to pick (multiple) images on Android 4.3 or higher. + No configuration required - the plugin should work out of the box. It is no longer required to add `android:requestLegacyExternalStorage="true"` as an attribute to the `` tag in AndroidManifest.xml, as `image_picker` has been updated to make use of scoped storage. @@ -63,6 +68,8 @@ Future retrieveLostData() async { There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it. +On Android, `getLostData` will only get the last picked image when picking multiple images, see: [#84634](https://github.com/flutter/flutter/issues/84634). + ## Deprecation warnings in `pickImage`, `pickVideo` and `LostDataResponse` Starting with version **0.6.7** of the image_picker plugin, the API of the plugin changed slightly to allow for web implementations to exist. diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index c934b54a1f8e..c4a686f5ce13 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -22,6 +22,7 @@ import io.flutter.plugin.common.PluginRegistry; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; @@ -75,6 +76,7 @@ public class ImagePickerDelegate @VisibleForTesting static final int REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY = 2342; @VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343; @VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345; + @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352; @VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353; @VisibleForTesting static final int REQUEST_CAMERA_VIDEO_PERMISSION = 2355; @@ -315,6 +317,15 @@ public void chooseImageFromGallery(MethodCall methodCall, MethodChannel.Result r launchPickImageFromGalleryIntent(); } + public void chooseMultiImageFromGallery(MethodCall methodCall, MethodChannel.Result result) { + if (!setPendingMethodCallAndResult(methodCall, result)) { + finishWithAlreadyActiveError(result); + return; + } + + launchMultiPickImageFromGalleryIntent(); + } + private void launchPickImageFromGalleryIntent() { Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT); pickImageIntent.setType("image/*"); @@ -322,6 +333,16 @@ private void launchPickImageFromGalleryIntent() { activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY); } + private void launchMultiPickImageFromGalleryIntent() { + Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } + pickImageIntent.setType("image/*"); + + activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY); + } + public void takeImageWithCamera(MethodCall methodCall, MethodChannel.Result result) { if (!setPendingMethodCallAndResult(methodCall, result)) { finishWithAlreadyActiveError(result); @@ -440,6 +461,9 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY: handleChooseImageResult(resultCode, data); break; + case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY: + handleChooseMultiImageResult(resultCode, data); + break; case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA: handleCaptureImageResult(resultCode); break; @@ -467,6 +491,24 @@ private void handleChooseImageResult(int resultCode, Intent data) { finishWithSuccess(null); } + private void handleChooseMultiImageResult(int resultCode, Intent intent) { + if (resultCode == Activity.RESULT_OK && intent != null) { + ArrayList paths = new ArrayList<>(); + if (intent.getClipData() != null) { + for (int i = 0; i < intent.getClipData().getItemCount(); i++) { + paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri())); + } + } else { + paths.add(fileUtils.getPathFromUri(activity, intent.getData())); + } + handleMultiImageResult(paths, false); + return; + } + + // User cancelled choosing a picture. + finishWithSuccess(null); + } + private void handleChooseVideoResult(int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { String path = fileUtils.getPathFromUri(activity, data.getData()); @@ -516,26 +558,45 @@ public void onPathReady(String path) { finishWithSuccess(null); } - private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { + private void handleMultiImageResult( + ArrayList paths, boolean shouldDeleteOriginalIfScaled) { if (methodCall != null) { - Double maxWidth = methodCall.argument("maxWidth"); - Double maxHeight = methodCall.argument("maxHeight"); - Integer imageQuality = methodCall.argument("imageQuality"); - - String finalImagePath = - imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality); - - finishWithSuccess(finalImagePath); + for (int i = 0; i < paths.size(); i++) { + String finalImagePath = getResizedImagePath(paths.get(i)); + + //delete original file if scaled + if (finalImagePath != null + && !finalImagePath.equals(paths.get(i)) + && shouldDeleteOriginalIfScaled) { + new File(paths.get(i)).delete(); + } + paths.set(i, finalImagePath); + } + finishWithListSuccess(paths); + } + } + private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { + if (methodCall != null) { + String finalImagePath = getResizedImagePath(path); //delete original file if scaled if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) { new File(path).delete(); } + finishWithSuccess(finalImagePath); } else { finishWithSuccess(path); } } + private String getResizedImagePath(String path) { + Double maxWidth = methodCall.argument("maxWidth"); + Double maxHeight = methodCall.argument("maxHeight"); + Integer imageQuality = methodCall.argument("imageQuality"); + + return imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality); + } + private void handleVideoResult(String path) { finishWithSuccess(path); } @@ -564,6 +625,17 @@ private void finishWithSuccess(String imagePath) { clearMethodCallAndResult(); } + private void finishWithListSuccess(ArrayList imagePaths) { + if (pendingResult == null) { + for (String imagePath : imagePaths) { + cache.saveResult(imagePath, null, null); + } + return; + } + pendingResult.success(imagePaths); + clearMethodCallAndResult(); + } + private void finishWithAlreadyActiveError(MethodChannel.Result result) { result.error("already_active", "Image picker is already active", null); } diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index bffc903b531e..577675bd433a 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -91,6 +91,7 @@ public void onActivityStopped(Activity activity) { } static final String METHOD_CALL_IMAGE = "pickImage"; + static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage"; static final String METHOD_CALL_VIDEO = "pickVideo"; private static final String METHOD_CALL_RETRIEVE = "retrieve"; private static final int CAMERA_DEVICE_FRONT = 1; @@ -302,6 +303,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) { throw new IllegalArgumentException("Invalid image source: " + imageSource); } break; + case METHOD_CALL_MULTI_IMAGE: + delegate.chooseMultiImageFromGallery(call, result); + break; case METHOD_CALL_VIDEO: imageSource = call.argument("source"); switch (imageSource) { diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index da53b10b50f5..f8be66833b17 100644 --- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -104,6 +104,15 @@ public void chooseImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyAc } @Test + public void chooseMultiImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyActiveError() { + ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); + + delegate.chooseMultiImageFromGallery(mockMethodCall, mockResult); + + verifyFinishedWithAlreadyActiveError(); + verifyNoMoreInteractions(mockResult); + } + public void chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index a0ce87f4f2b7..422b8be74f7c 100644 --- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -32,6 +32,7 @@ public class ImagePickerPluginTest { private static final int SOURCE_CAMERA = 0; private static final int SOURCE_GALLERY = 1; private static final String PICK_IMAGE = "pickImage"; + private static final String PICK_MULTI_IMAGE = "pickMultiImage"; private static final String PICK_VIDEO = "pickVideo"; @Rule public ExpectedException exception = ExpectedException.none(); @@ -92,6 +93,14 @@ public void onMethodCall_WhenSourceIsGallery_InvokesChooseImageFromGallery() { verifyZeroInteractions(mockResult); } + @Test + public void onMethodCall_InvokesChooseMultiImageFromGallery() { + MethodCall call = buildMethodCall(PICK_MULTI_IMAGE); + plugin.onMethodCall(call, mockResult); + verify(mockImagePickerDelegate).chooseMultiImageFromGallery(eq(call), any()); + verifyZeroInteractions(mockResult); + } + @Test public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() { MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA); @@ -173,4 +182,8 @@ private MethodCall buildMethodCall(String method, final int source) { return new MethodCall(method, arguments); } + + private MethodCall buildMethodCall(String method) { + return new MethodCall(method, null); + } } diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m index 04ba4b98e241..f667526671f7 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -22,7 +22,7 @@ - (UIViewController *)presentedViewController { @interface FLTImagePickerPlugin (Test) @property(copy, nonatomic) FlutterResult result; -- (void)handleSavedPath:(NSString *)path; +- (void)handleSavedPathList:(NSMutableArray *)pathList; - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker; @end @@ -122,21 +122,6 @@ - (void)testPickingVideoWithDuration { XCTAssertEqual([plugin getImagePickerController].videoMaximumDuration, 95); } -- (void)testPluginPickImageSelectMultipleTimes { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - plugin.result = ^(id result) { - - }; - [plugin handleSavedPath:@"test"]; - [plugin handleSavedPath:@"test"]; -} - - (void)testViewController { UIWindow *window = [UIWindow new]; MockViewController *vc1 = [MockViewController new]; @@ -149,4 +134,62 @@ - (void)testViewController { XCTAssertEqual([plugin viewControllerWithWindow:window], vc2); } +- (void)testPluginMultiImagePathIsNil { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + + dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); + __block FlutterError *pickImageResult = nil; + + plugin.result = ^(id _Nullable r) { + pickImageResult = r; + dispatch_semaphore_signal(resultSemaphore); + }; + [plugin handleSavedPathList:nil]; + + dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); + + XCTAssertEqualObjects(pickImageResult.code, @"create_error"); +} + +- (void)testPluginMultiImagePathHasNullItem { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + NSMutableArray *pathList = [NSMutableArray new]; + + [pathList addObject:[NSNull null]]; + + dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); + __block FlutterError *pickImageResult = nil; + + plugin.result = ^(id _Nullable r) { + pickImageResult = r; + dispatch_semaphore_signal(resultSemaphore); + }; + [plugin handleSavedPathList:pathList]; + + dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); + + XCTAssertEqualObjects(pickImageResult.code, @"create_error"); +} + +- (void)testPluginMultiImagePathHasItem { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + NSString *savedPath = @"test"; + NSMutableArray *pathList = [NSMutableArray new]; + + [pathList addObject:savedPath]; + + dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); + __block id pickImageResult = nil; + + plugin.result = ^(id _Nullable r) { + pickImageResult = r; + dispatch_semaphore_signal(resultSemaphore); + }; + [plugin handleSavedPathList:pathList]; + + dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); + + XCTAssertEqual(pickImageResult, pathList); +} + @end diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index e3df6413e9a8..7c91606ba535 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m @@ -14,6 +14,7 @@ #import "FLTImagePickerImageUtil.h" #import "FLTImagePickerMetaDataUtil.h" #import "FLTImagePickerPhotoAssetUtil.h" +#import "FLTPHPickerSaveImageToPathOperation.h" @interface FLTImagePickerPlugin () *)results API_AVAILABLE(ios(14)) { [picker dismissViewControllerAnimated:YES completion:nil]; + dispatch_queue_t backgroundQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); + dispatch_async(backgroundQueue, ^{ + if (results.count == 0) { + self.result(nil); + self.result = nil; + self->_arguments = nil; + return; + } + NSNumber *maxWidth = [self->_arguments objectForKey:@"maxWidth"]; + NSNumber *maxHeight = [self->_arguments objectForKey:@"maxHeight"]; + NSNumber *imageQuality = [self->_arguments objectForKey:@"imageQuality"]; + NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; + NSOperationQueue *operationQueue = [NSOperationQueue new]; + NSMutableArray *pathList = [self createNSMutableArrayWithSize:results.count]; - NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"]; - NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"]; - NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"]; - NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; - - for (PHPickerResult *result in results) { - [result.itemProvider - loadObjectOfClass:[UIImage class] - completionHandler:^(__kindof id _Nullable image, - NSError *_Nullable error) { - if ([image isKindOfClass:[UIImage class]]) { - __block UIImage *localImage = image; - dispatch_async(dispatch_get_main_queue(), ^{ - PHAsset *originalAsset = - [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:result]; - - if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { - localImage = [FLTImagePickerImageUtil scaledImage:localImage - maxWidth:maxWidth - maxHeight:maxHeight - isMetadataAvailable:originalAsset != nil]; - } - - if (!originalAsset) { - // Image picked without an original asset (e.g. User took a photo directly) - [self saveImageWithPickerInfo:nil - image:localImage - imageQuality:desiredImageQuality]; - } else { - [[PHImageManager defaultManager] - requestImageDataForAsset:originalAsset - options:nil - resultHandler:^( - NSData *_Nullable imageData, NSString *_Nullable dataUTI, - UIImageOrientation orientation, NSDictionary *_Nullable info) { - // maxWidth and maxHeight are used only for GIF images. - [self saveImageWithOriginalImageData:imageData - image:localImage - maxWidth:maxWidth + for (int i = 0; i < results.count; i++) { + PHPickerResult *result = results[i]; + FLTPHPickerSaveImageToPathOperation *operation = + [[FLTPHPickerSaveImageToPathOperation alloc] initWithResult:result maxHeight:maxHeight - imageQuality:desiredImageQuality]; - }]; - } - }); - } - }]; - } + maxWidth:maxWidth + desiredImageQuality:desiredImageQuality + savedPathBlock:^(NSString *savedPath) { + pathList[i] = savedPath; + }]; + [operationQueue addOperation:operation]; + } + [operationQueue waitUntilAllOperationsAreFinished]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleSavedPathList:pathList]; + }); + }); +} + +/** + * Creates an NSMutableArray of a certain size filled with NSNull objects. + * + * The difference with initWithCapacity is that initWithCapacity still gives an empty array making + * it impossible to add objects on an index larger than the size. + * + * @param @size The length of the required array + * @return @NSMutableArray An array of a specified size + */ +- (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { + NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size]; + for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++) + ; + return mutableArray; } - (void)imagePickerController:(UIImagePickerController *)picker @@ -504,7 +512,7 @@ - (void)saveImageWithOriginalImageData:(NSData *)originalImageData maxWidth:maxWidth maxHeight:maxHeight imageQuality:imageQuality]; - [self handleSavedPath:savedPath]; + [self handleSavedPathList:@[ savedPath ]]; } - (void)saveImageWithPickerInfo:(NSDictionary *)info @@ -513,18 +521,43 @@ - (void)saveImageWithPickerInfo:(NSDictionary *)info NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; - [self handleSavedPath:savedPath]; + [self handleSavedPathList:@[ savedPath ]]; } -- (void)handleSavedPath:(NSString *)path { +/** + * Applies NSMutableArray on the FLutterResult. + * + * NSString must be returned by FlutterResult if the single image + * mode is active. It is checked by @c maxImagesAllowed and + * returns the first object of the @c pathlist. + * + * NSMutableArray must be returned by FlutterResult if the multi-image + * mode is active. After the @c pathlist count is checked then it returns + * the @c pathlist. + * + * @param @pathList that should be applied to FlutterResult. + */ +- (void)handleSavedPathList:(NSArray *)pathList { if (!self.result) { return; } - if (path) { - self.result(path); + + if (pathList) { + if (![pathList containsObject:[NSNull null]]) { + if ((self.maxImagesAllowed == 1)) { + self.result(pathList.firstObject); + } else { + self.result(pathList); + } + } else { + self.result([FlutterError errorWithCode:@"create_error" + message:@"pathList's items should not be null" + details:nil]); + } } else { + // This should never happen. self.result([FlutterError errorWithCode:@"create_error" - message:@"Temporary file could not be created" + message:@"pathList should not be nil" details:nil]); } self.result = nil; diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h new file mode 100644 index 000000000000..7ba3d28ef3dd --- /dev/null +++ b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h @@ -0,0 +1,31 @@ +// 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. + +#import +#import + +#import "FLTImagePickerImageUtil.h" +#import "FLTImagePickerMetaDataUtil.h" +#import "FLTImagePickerPhotoAssetUtil.h" + +/*! + @class FLTPHPickerSaveImageToPathOperation + + @brief The FLTPHPickerSaveImageToPathOperation class + + @discussion This class was implemented to handle saved image paths and populate the pathList + with the final result by using GetSavedPath type block. + + @superclass SuperClass: NSOperation\n + @helps It helps FLTImagePickerPlugin class. + */ +@interface FLTPHPickerSaveImageToPathOperation : NSOperation + +- (instancetype)initWithResult:(PHPickerResult *)result + maxHeight:(NSNumber *)maxHeight + maxWidth:(NSNumber *)maxWidth + desiredImageQuality:(NSNumber *)desiredImageQuality + savedPathBlock:(void (^)(NSString *))savedPathBlock API_AVAILABLE(ios(14)); + +@end diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m new file mode 100644 index 000000000000..30da22774d07 --- /dev/null +++ b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -0,0 +1,132 @@ +// 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. + +#import "FLTPHPickerSaveImageToPathOperation.h" + +API_AVAILABLE(ios(14)) +@interface FLTPHPickerSaveImageToPathOperation () + +@property(strong, nonatomic) PHPickerResult *result; +@property(assign, nonatomic) NSNumber *maxHeight; +@property(assign, nonatomic) NSNumber *maxWidth; +@property(assign, nonatomic) NSNumber *desiredImageQuality; + +@end + +typedef void (^GetSavedPath)(NSString *); + +@implementation FLTPHPickerSaveImageToPathOperation { + BOOL executing; + BOOL finished; + GetSavedPath getSavedPath; +} + +- (instancetype)initWithResult:(PHPickerResult *)result + maxHeight:(NSNumber *)maxHeight + maxWidth:(NSNumber *)maxWidth + desiredImageQuality:(NSNumber *)desiredImageQuality + savedPathBlock:(GetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) { + if (self = [super init]) { + if (result) { + self.result = result; + self.maxHeight = maxHeight; + self.maxWidth = maxWidth; + self.desiredImageQuality = desiredImageQuality; + getSavedPath = savedPathBlock; + executing = NO; + finished = NO; + } else { + return nil; + } + return self; + } else { + return nil; + } +} + +- (BOOL)isConcurrent { + return YES; +} + +- (BOOL)isExecuting { + return executing; +} + +- (BOOL)isFinished { + return finished; +} + +- (void)setFinished:(BOOL)isFinished { + [self willChangeValueForKey:@"isFinished"]; + self->finished = isFinished; + [self didChangeValueForKey:@"isFinished"]; +} + +- (void)setExecuting:(BOOL)isExecuting { + [self willChangeValueForKey:@"isExecuting"]; + self->executing = isExecuting; + [self didChangeValueForKey:@"isExecuting"]; +} + +- (void)completeOperationWithPath:(NSString *)savedPath { + [self setExecuting:NO]; + [self setFinished:YES]; + getSavedPath(savedPath); +} + +- (void)start { + if ([self isCancelled]) { + [self setFinished:YES]; + return; + } + if (@available(iOS 14, *)) { + [self setExecuting:YES]; + [self.result.itemProvider + loadObjectOfClass:[UIImage class] + completionHandler:^(__kindof id _Nullable image, + NSError *_Nullable error) { + if ([image isKindOfClass:[UIImage class]]) { + __block UIImage *localImage = image; + PHAsset *originalAsset = + [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result]; + + if (self.maxWidth != (id)[NSNull null] || self.maxHeight != (id)[NSNull null]) { + localImage = [FLTImagePickerImageUtil scaledImage:localImage + maxWidth:self.maxWidth + maxHeight:self.maxHeight + isMetadataAvailable:originalAsset != nil]; + } + __block NSString *savedPath; + if (!originalAsset) { + // Image picked without an original asset (e.g. User pick image without permission) + savedPath = + [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil + image:localImage + imageQuality:self.desiredImageQuality]; + [self completeOperationWithPath:savedPath]; + } else { + [[PHImageManager defaultManager] + requestImageDataForAsset:originalAsset + options:nil + resultHandler:^( + NSData *_Nullable imageData, NSString *_Nullable dataUTI, + UIImageOrientation orientation, NSDictionary *_Nullable info) { + // maxWidth and maxHeight are used only for GIF images. + savedPath = [FLTImagePickerPhotoAssetUtil + saveImageWithOriginalImageData:imageData + image:localImage + maxWidth:self.maxWidth + maxHeight:self.maxHeight + imageQuality:self.desiredImageQuality]; + [self completeOperationWithPath:savedPath]; + }]; + } + } + }]; + } else { + [self setFinished:YES]; + } +} + +@end diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 77c26d40346a..f4dee93ee1d6 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -54,6 +54,8 @@ class ImagePicker { /// /// 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 [getLostData] when your app relaunches to retrieve the lost data. + /// + /// See also [getMultiImage] to allow users to select multiple images at once. Future getImage({ required ImageSource source, double? maxWidth, @@ -70,6 +72,37 @@ class ImagePicker { ); } + /// Returns a [List] object wrapping the images that were picked. + /// + /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// + /// 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. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// 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. + /// + /// See also [getImage] to allow users to only pick a single image. + Future?> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + return platform.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + } + /// Returns a [PickedFile] object wrapping the video that was picked. /// /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 4dc7785111a4..b8aa9337a30c 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/master/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.0+4 +version: 0.8.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -24,8 +24,8 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - image_picker_platform_interface: ^2.0.0 image_picker_for_web: ^2.0.0 + image_picker_platform_interface: ^2.1.0 dev_dependencies: flutter_test: 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 f56d47ff262b..d83b403d1d45 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -20,15 +20,6 @@ void main() { final picker = ImagePicker(); - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); - - log.clear(); - }); - test('ImagePicker platform instance overrides the actual platform used', () { final ImagePickerPlatform savedPlatform = ImagePickerPlatform.instance; @@ -38,312 +29,420 @@ void main() { ImagePickerPlatform.instance = savedPlatform; }); - group('#pickImage', () { - test('passes the image source argument correctly', () async { - await picker.getImage(source: ImageSource.camera); - await picker.getImage(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - ], - ); + group('#Single image/video', () { + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + + log.clear(); }); - test('passes the width and height arguments correctly', () async { - await picker.getImage(source: ImageSource.camera); - await picker.getImage( - source: ImageSource.camera, - maxWidth: 10.0, - ); - await picker.getImage( - source: ImageSource.camera, - maxHeight: 10.0, - ); - await picker.getImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - ); - await picker.getImage( - source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); - await picker.getImage( - source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); - await picker.getImage( + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.getImage( source: ImageSource.camera, maxWidth: 10.0, maxHeight: 20.0, - imageQuality: 70); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - ], - ); - }); + ); + await picker.getImage( + source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); + await picker.getImage( + source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + ], + ); + }); - test('does not accept a negative width or height argument', () { - expect( - picker.getImage(source: ImageSource.camera, maxWidth: -1.0), - throwsArgumentError, - ); + test('does not accept a negative width or height argument', () { + expect( + picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); - expect( - picker.getImage(source: ImageSource.camera, maxHeight: -1.0), - throwsArgumentError, - ); - }); + expect( + picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); - test('handles a null image path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); - expect(await picker.getImage(source: ImageSource.gallery), isNull); - expect(await picker.getImage(source: ImageSource.camera), isNull); - }); + expect(await picker.getImage(source: ImageSource.gallery), isNull); + expect(await picker.getImage(source: ImageSource.camera), isNull); + }); - test('camera position defaults to back', () async { - await picker.getImage(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - }), - ], - ); - }); + test('camera position defaults to back', () async { + await picker.getImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); - test('camera position can set to front', () async { - await picker.getImage( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - }), - ], - ); + test('camera position can set to front', () async { + await picker.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); }); - }); - group('#pickVideo', () { - test('passes the image source argument correctly', () async { - await picker.getVideo(source: ImageSource.camera); - await picker.getVideo(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); - test('passes the duration argument correctly', () async { - await picker.getVideo(source: ImageSource.camera); - await picker.getVideo( - source: ImageSource.camera, - maxDuration: const Duration(seconds: 10)); - await picker.getVideo( - source: ImageSource.camera, - maxDuration: const Duration(minutes: 1)); - await picker.getVideo( - source: ImageSource.camera, maxDuration: const Duration(hours: 1)); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - }), - ], - ); - }); + test('passes the duration argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10)); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1)); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1)); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); - test('handles a null video path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + test('handles a null video path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); - expect(await picker.getVideo(source: ImageSource.gallery), isNull); - expect(await picker.getVideo(source: ImageSource.camera), isNull); - }); + expect(await picker.getVideo(source: ImageSource.gallery), isNull); + expect(await picker.getVideo(source: ImageSource.camera), isNull); + }); - test('camera position defaults to back', () async { - await picker.getVideo(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); + test('camera position defaults to back', () async { + await picker.getVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); }); - test('camera position can set to front', () async { - await picker.getVideo( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - }), - ], - ); + group('#retrieveLostData', () { + test('retrieveLostData get success response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostData response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file!.path, '/example/path'); + }); + + test('retrieveLostData get error response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostData response = await picker.getLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('retrieveLostData get null response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.getLostData()).isEmpty, true); + }); + + test('retrieveLostData get both path and error should throw', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.getLostData(), throwsAssertionError); + }); }); }); - group('#retrieveLostData', () { - test('retrieveLostData get success response', () async { + group('Multi images', () { + setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; + log.add(methodCall); + return []; }); - final LostData response = await picker.getLostData(); - expect(response.type, RetrieveType.image); - expect(response.file!.path, '/example/path'); + log.clear(); }); - test('retrieveLostData get error response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; + group('#pickMultiImage', () { + test('passes the width and height arguments correctly', () async { + await picker.getMultiImage(); + await picker.getMultiImage( + maxWidth: 10.0, + ); + await picker.getMultiImage( + maxHeight: 10.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); }); - final LostData response = await picker.getLostData(); - expect(response.type, RetrieveType.video); - expect(response.exception!.code, 'test_error_code'); - expect(response.exception!.message, 'test_error_message'); - }); - test('retrieveLostData get null response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; + test('does not accept a negative width or height argument', () { + expect( + picker.getMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + picker.getMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); }); - expect((await picker.getLostData()).isEmpty, true); - }); - test('retrieveLostData get both path and error should throw', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); }); - expect(picker.getLostData(), throwsAssertionError); }); }); });