[image_picker_platform_interface] Added pickMultiImage (#3782)

* Added pickMultiImage to image_picker_platform_interface

* Added tests

* fixed platform_interface tests

* Added tests

* fixed platform_interface tests

* Fixed tests

* Fixed version in pubspec.yaml

* Added test for imageQuality value; Implemented feedback

* Format
diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
index 598f83b..e2def72 100644
--- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
+++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.1.0
+
+* Add `pickMultiImage` method.
+
 ## 2.0.1
 
 * Update platform_plugin_interface version requirement.
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
index 429c51b..e0f4645 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
@@ -36,6 +36,54 @@
     return path != null ? PickedFile(path) : null;
   }
 
+  @override
+  Future<List<PickedFile>?> pickMultiImage({
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+  }) async {
+    final List<dynamic>? paths = await _pickMultiImagePath(
+      maxWidth: maxWidth,
+      maxHeight: maxHeight,
+      imageQuality: imageQuality,
+    );
+    if (paths == null) return null;
+
+    final List<PickedFile> files = [];
+    for (final path in paths) {
+      files.add(PickedFile(path));
+    }
+    return files;
+  }
+
+  Future<List<dynamic>?> _pickMultiImagePath({
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+  }) {
+    if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
+      throw ArgumentError.value(
+          imageQuality, 'imageQuality', 'must be between 0 and 100');
+    }
+
+    if (maxWidth != null && maxWidth < 0) {
+      throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative');
+    }
+
+    if (maxHeight != null && maxHeight < 0) {
+      throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative');
+    }
+
+    return _channel.invokeMethod<List<dynamic>?>(
+      'pickMultiImage',
+      <String, dynamic>{
+        'maxWidth': maxWidth,
+        'maxHeight': maxHeight,
+        'imageQuality': imageQuality,
+      },
+    );
+  }
+
   Future<String?> _pickImagePath({
     required ImageSource source,
     double? maxWidth,
@@ -74,7 +122,7 @@
     CameraDevice preferredCameraDevice = CameraDevice.rear,
     Duration? maxDuration,
   }) async {
-    String? path = await _pickVideoPath(
+    final String? path = await _pickVideoPath(
       source: source,
       maxDuration: maxDuration,
       preferredCameraDevice: preferredCameraDevice,
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
index 3bbf422..32af774 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
@@ -54,9 +54,9 @@
   ///
   /// 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 supportted for certain
+  /// the original quality will be returned. Compression is only supported for certain
   /// image types such as JPEG. If compression is not supported for the image that is picked,
-  /// an warning message will be logged.
+  /// 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.
@@ -78,6 +78,32 @@
     throw UnimplementedError('pickImage() has not been implemented.');
   }
 
+  /// Returns a [List<PickedFile>] with the images that were picked.
+  ///
+  /// The images come from the [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.
+  ///
+  /// If specified, the image will be at most `maxWidth` wide and
+  /// `maxHeight` tall. Otherwise the image 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. If compression is not supported for the image that is picked,
+  /// a warning message will be logged.
+  ///
+  /// If no images were picked, the return value is null.
+  Future<List<PickedFile>?> pickMultiImage({
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+  }) {
+    throw UnimplementedError('pickMultiImage() has not been implemented.');
+  }
+
   /// Returns a [PickedFile] containing the video that was picked.
   ///
   /// The [source] argument controls where the video comes from. This can
diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml
index 3443f15..bd197a6 100644
--- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml
+++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml
@@ -3,7 +3,7 @@
 homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_platform_interface
 # NOTE: We strongly prefer non-breaking changes, even at the expense of a
 # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 2.0.1
+version: 2.1.0
 
 dependencies:
   flutter:
diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart
index 2621733..83ae6fa 100644
--- a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart
+++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart
@@ -15,11 +15,13 @@
     MethodChannelImagePicker picker = MethodChannelImagePicker();
 
     final List<MethodCall> log = <MethodCall>[];
+    dynamic returnValue = '';
 
     setUp(() {
+      returnValue = '';
       picker.channel.setMockMethodCallHandler((MethodCall methodCall) async {
         log.add(methodCall);
-        return '';
+        return returnValue;
       });
 
       log.clear();
@@ -139,6 +141,29 @@
         );
       });
 
+      test('does not accept a invalid imageQuality argument', () {
+        expect(
+          () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery),
+          throwsArgumentError,
+        );
+
+        expect(
+          () =>
+              picker.pickImage(imageQuality: 101, source: ImageSource.gallery),
+          throwsArgumentError,
+        );
+
+        expect(
+          () => picker.pickImage(imageQuality: -1, source: ImageSource.camera),
+          throwsArgumentError,
+        );
+
+        expect(
+          () => picker.pickImage(imageQuality: 101, source: ImageSource.camera),
+          throwsArgumentError,
+        );
+      });
+
       test('does not accept a negative width or height argument', () {
         expect(
           () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0),
@@ -196,6 +221,127 @@
       });
     });
 
+    group('#pickMultiImage', () {
+      test('calls the method correctly', () async {
+        returnValue = ['0', '1'];
+        await picker.pickMultiImage();
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': null,
+              'imageQuality': null,
+            }),
+          ],
+        );
+      });
+
+      test('passes the width and height arguments correctly', () async {
+        returnValue = ['0', '1'];
+        await picker.pickMultiImage();
+        await picker.pickMultiImage(
+          maxWidth: 10.0,
+        );
+        await picker.pickMultiImage(
+          maxHeight: 10.0,
+        );
+        await picker.pickMultiImage(
+          maxWidth: 10.0,
+          maxHeight: 20.0,
+        );
+        await picker.pickMultiImage(
+          maxWidth: 10.0,
+          imageQuality: 70,
+        );
+        await picker.pickMultiImage(
+          maxHeight: 10.0,
+          imageQuality: 70,
+        );
+        await picker.pickMultiImage(
+          maxWidth: 10.0,
+          maxHeight: 20.0,
+          imageQuality: 70,
+        );
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': null,
+              'imageQuality': null,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': 10.0,
+              'maxHeight': null,
+              'imageQuality': null,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': 10.0,
+              'imageQuality': null,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': 10.0,
+              'maxHeight': 20.0,
+              'imageQuality': null,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': 10.0,
+              'maxHeight': null,
+              'imageQuality': 70,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': 10.0,
+              'imageQuality': 70,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': 10.0,
+              'maxHeight': 20.0,
+              'imageQuality': 70,
+            }),
+          ],
+        );
+      });
+
+      test('does not accept a negative width or height argument', () {
+        returnValue = ['0', '1'];
+        expect(
+          () => picker.pickMultiImage(maxWidth: -1.0),
+          throwsArgumentError,
+        );
+
+        expect(
+          () => picker.pickMultiImage(maxHeight: -1.0),
+          throwsArgumentError,
+        );
+      });
+
+      test('does not accept a invalid imageQuality argument', () {
+        returnValue = ['0', '1'];
+        expect(
+          () => picker.pickMultiImage(imageQuality: -1),
+          throwsArgumentError,
+        );
+
+        expect(
+          () => picker.pickMultiImage(imageQuality: 101),
+          throwsArgumentError,
+        );
+      });
+
+      test('handles a null image path response gracefully', () async {
+        picker.channel
+            .setMockMethodCallHandler((MethodCall methodCall) => null);
+
+        expect(await picker.pickMultiImage(), isNull);
+        expect(await picker.pickMultiImage(), isNull);
+      });
+    });
+
     group('#pickVideoPath', () {
       test('passes the image source argument correctly', () async {
         await picker.pickVideo(source: ImageSource.camera);