[image_picker] add requestFullMetadata for iOS (optional permissions) - platform interface changes for multi image picking (#5914)

Platform interface changes for #5915 - adding possibility to disable full metadata when picking multiple images
diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
index 0a4e98b..120b7b0 100644
--- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
+++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 2.6.0
+
+* Deprecates `getMultiImage` in favor of a new method `getMultiImageWithOptions`.
+    * Adds `requestFullMetadata` option that allows disabling extra permission requests
+      on certain platforms.
+    * Moves optional image picking parameters to `MultiImagePickerOptions` class.
+
 ## 2.5.0
 
 * Deprecates `getImage` in favor of a new method `getImageFromSource`.
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 ba5d60d..d215fa2 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
@@ -8,6 +8,7 @@
 import 'package:flutter/services.dart';
 
 import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+import 'package:image_picker_platform_interface/src/types/multi_image_picker_options.dart';
 
 const MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker');
 
@@ -57,6 +58,7 @@
     double? maxWidth,
     double? maxHeight,
     int? imageQuality,
+    bool requestFullMetadata = true,
   }) {
     if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) {
       throw ArgumentError.value(
@@ -77,6 +79,7 @@
         'maxWidth': maxWidth,
         'maxHeight': maxHeight,
         'imageQuality': imageQuality,
+        'requestFullMetadata': requestFullMetadata,
       },
     );
   }
@@ -234,6 +237,23 @@
   }
 
   @override
+  Future<List<XFile>> getMultiImageWithOptions({
+    MultiImagePickerOptions options = const MultiImagePickerOptions(),
+  }) async {
+    final List<dynamic>? paths = await _getMultiImagePath(
+      maxWidth: options.imageOptions.maxWidth,
+      maxHeight: options.imageOptions.maxHeight,
+      imageQuality: options.imageOptions.imageQuality,
+      requestFullMetadata: options.imageOptions.requestFullMetadata,
+    );
+    if (paths == null) {
+      return <XFile>[];
+    }
+
+    return paths.map((dynamic path) => XFile(path as String)).toList();
+  }
+
+  @override
   Future<XFile?> getVideo({
     required ImageSource source,
     CameraDevice preferredCameraDevice = CameraDevice.rear,
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 d1d06f9..a2618d5 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
@@ -6,6 +6,7 @@
 
 import 'package:cross_file/cross_file.dart';
 import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart';
+import 'package:image_picker_platform_interface/src/types/multi_image_picker_options.dart';
 import 'package:image_picker_platform_interface/src/types/types.dart';
 import 'package:plugin_platform_interface/plugin_platform_interface.dart';
 
@@ -186,6 +187,8 @@
     throw UnimplementedError('getImage() has not been implemented.');
   }
 
+  /// This method is deprecated in favor of [getMultiImageWithOptions] and will be removed in a future update.
+  ///
   /// Returns a [List<XFile>] with the images that were picked.
   ///
   /// The images come from the [ImageSource.gallery].
@@ -283,4 +286,23 @@
       preferredCameraDevice: options.preferredCameraDevice,
     );
   }
+
+  /// Returns a [List<XFile>] with the images that were picked.
+  ///
+  /// The images come from the [ImageSource.gallery].
+  ///
+  /// The `options` argument controls additional settings that can be used when
+  /// picking an image. See [MultiImagePickerOptions] for more details.
+  ///
+  /// If no images were picked, returns an empty list.
+  Future<List<XFile>> getMultiImageWithOptions({
+    MultiImagePickerOptions options = const MultiImagePickerOptions(),
+  }) async {
+    final List<XFile>? pickedImages = await getMultiImage(
+      maxWidth: options.imageOptions.maxWidth,
+      maxHeight: options.imageOptions.maxHeight,
+      imageQuality: options.imageOptions.imageQuality,
+    );
+    return pickedImages ?? <XFile>[];
+  }
 }
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart
new file mode 100644
index 0000000..2cc01c9
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart
@@ -0,0 +1,41 @@
+// 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.
+
+/// Specifies image-specific options for picking.
+class ImageOptions {
+  /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality]
+  /// and [requestFullMetadata].
+  const ImageOptions({
+    this.maxHeight,
+    this.maxWidth,
+    this.imageQuality,
+    this.requestFullMetadata = true,
+  });
+
+  /// The maximum width of the image, in pixels.
+  ///
+  /// If null, the image will only be resized if [maxHeight] is specified.
+  final double? maxWidth;
+
+  /// The maximum height of the image, in pixels.
+  ///
+  /// If null, the image will only be resized if [maxWidth] is specified.
+  final double? maxHeight;
+
+  /// Modifies the quality of the image, ranging from 0-100 where 100 is the
+  /// original/max quality.
+  ///
+  /// 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 null, the image will be returned with the original quality.
+  final int? imageQuality;
+
+  /// If true, requests full image metadata, which may require extra permissions
+  /// on some platforms, (e.g., NSPhotoLibraryUsageDescription on iOS).
+  //
+  // Defaults to true.
+  final bool requestFullMetadata;
+}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart
new file mode 100644
index 0000000..4d7971c
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart
@@ -0,0 +1,16 @@
+// 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 'package:image_picker_platform_interface/src/types/image_options.dart';
+
+/// Specifies options for picking multiple images from the device's gallery.
+class MultiImagePickerOptions {
+  /// Creates an instance with the given [imageOptions].
+  const MultiImagePickerOptions({
+    this.imageOptions = const ImageOptions(),
+  });
+
+  /// The image-specific options for picking.
+  final ImageOptions imageOptions;
+}
diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml
index 4ce1d2f..50d84f8 100644
--- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml
+++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml
@@ -4,7 +4,7 @@
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
 # 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.5.0
+version: 2.6.0
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
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 72ed363..27d7016 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
@@ -7,6 +7,8 @@
 
 import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
 import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart';
+import 'package:image_picker_platform_interface/src/types/image_options.dart';
+import 'package:image_picker_platform_interface/src/types/multi_image_picker_options.dart';
 
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
@@ -244,6 +246,7 @@
               'maxWidth': null,
               'maxHeight': null,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
           ],
         );
@@ -283,36 +286,43 @@
               'maxWidth': null,
               'maxHeight': null,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': 10.0,
               'maxHeight': null,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': null,
               'maxHeight': 10.0,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': 10.0,
               'maxHeight': 20.0,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': 10.0,
               'maxHeight': null,
               'imageQuality': 70,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': null,
               'maxHeight': 10.0,
               'imageQuality': 70,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': 10.0,
               'maxHeight': 20.0,
               'imageQuality': 70,
+              'requestFullMetadata': true,
             }),
           ],
         );
@@ -723,6 +733,7 @@
               'maxWidth': null,
               'maxHeight': null,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
           ],
         );
@@ -762,36 +773,43 @@
               'maxWidth': null,
               'maxHeight': null,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': 10.0,
               'maxHeight': null,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': null,
               'maxHeight': 10.0,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': 10.0,
               'maxHeight': 20.0,
               'imageQuality': null,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': 10.0,
               'maxHeight': null,
               'imageQuality': 70,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': null,
               'maxHeight': 10.0,
               'imageQuality': 70,
+              'requestFullMetadata': true,
             }),
             isMethodCall('pickMultiImage', arguments: <String, dynamic>{
               'maxWidth': 10.0,
               'maxHeight': 20.0,
               'imageQuality': 70,
+              'requestFullMetadata': true,
             }),
           ],
         );
@@ -1257,5 +1275,211 @@
         );
       });
     });
+
+    group('#getMultiImageWithOptions', () {
+      test('calls the method correctly', () async {
+        returnValue = <dynamic>['0', '1'];
+        await picker.getMultiImageWithOptions();
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': null,
+              'imageQuality': null,
+              'requestFullMetadata': true,
+            }),
+          ],
+        );
+      });
+
+      test('passes the width, height and imageQuality arguments correctly',
+          () async {
+        returnValue = <dynamic>['0', '1'];
+        await picker.getMultiImageWithOptions();
+        await picker.getMultiImageWithOptions(
+          options: const MultiImagePickerOptions(
+            imageOptions: ImageOptions(maxWidth: 10.0),
+          ),
+        );
+        await picker.getMultiImageWithOptions(
+          options: const MultiImagePickerOptions(
+            imageOptions: ImageOptions(maxHeight: 10.0),
+          ),
+        );
+        await picker.getMultiImageWithOptions(
+          options: const MultiImagePickerOptions(
+            imageOptions: ImageOptions(
+              maxWidth: 10.0,
+              maxHeight: 20.0,
+            ),
+          ),
+        );
+        await picker.getMultiImageWithOptions(
+          options: const MultiImagePickerOptions(
+            imageOptions: ImageOptions(
+              maxWidth: 10.0,
+              imageQuality: 70,
+            ),
+          ),
+        );
+        await picker.getMultiImageWithOptions(
+          options: const MultiImagePickerOptions(
+            imageOptions: ImageOptions(
+              maxHeight: 10.0,
+              imageQuality: 70,
+            ),
+          ),
+        );
+        await picker.getMultiImageWithOptions(
+          options: const MultiImagePickerOptions(
+            imageOptions: ImageOptions(
+              maxWidth: 10.0,
+              maxHeight: 20.0,
+              imageQuality: 70,
+            ),
+          ),
+        );
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': null,
+              'imageQuality': null,
+              'requestFullMetadata': true,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': 10.0,
+              'maxHeight': null,
+              'imageQuality': null,
+              'requestFullMetadata': true,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': 10.0,
+              'imageQuality': null,
+              'requestFullMetadata': true,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': 10.0,
+              'maxHeight': 20.0,
+              'imageQuality': null,
+              'requestFullMetadata': true,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': 10.0,
+              'maxHeight': null,
+              'imageQuality': 70,
+              'requestFullMetadata': true,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': 10.0,
+              'imageQuality': 70,
+              'requestFullMetadata': true,
+            }),
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': 10.0,
+              'maxHeight': 20.0,
+              'imageQuality': 70,
+              'requestFullMetadata': true,
+            }),
+          ],
+        );
+      });
+
+      test('does not accept a negative width or height argument', () {
+        returnValue = <dynamic>['0', '1'];
+        expect(
+          () => picker.getMultiImageWithOptions(
+            options: const MultiImagePickerOptions(
+              imageOptions: ImageOptions(maxWidth: -1.0),
+            ),
+          ),
+          throwsArgumentError,
+        );
+
+        expect(
+          () => picker.getMultiImageWithOptions(
+            options: const MultiImagePickerOptions(
+              imageOptions: ImageOptions(maxHeight: -1.0),
+            ),
+          ),
+          throwsArgumentError,
+        );
+      });
+
+      test('does not accept an invalid imageQuality argument', () {
+        returnValue = <dynamic>['0', '1'];
+        expect(
+          () => picker.getMultiImageWithOptions(
+            options: const MultiImagePickerOptions(
+              imageOptions: ImageOptions(imageQuality: -1),
+            ),
+          ),
+          throwsArgumentError,
+        );
+
+        expect(
+          () => picker.getMultiImageWithOptions(
+            options: const MultiImagePickerOptions(
+              imageOptions: ImageOptions(imageQuality: 101),
+            ),
+          ),
+          throwsArgumentError,
+        );
+      });
+
+      test('handles a null image path response gracefully', () async {
+        picker.channel
+            .setMockMethodCallHandler((MethodCall methodCall) => null);
+
+        expect(await picker.getMultiImage(), isNull);
+        expect(await picker.getMultiImage(), isNull);
+      });
+
+      test('Request full metadata argument defaults to true', () async {
+        returnValue = <dynamic>['0', '1'];
+        await picker.getMultiImageWithOptions(
+          options: const MultiImagePickerOptions(),
+        );
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': null,
+              'imageQuality': null,
+              'requestFullMetadata': true,
+            }),
+          ],
+        );
+      });
+
+      test('passes the request full metadata argument correctly', () async {
+        returnValue = <dynamic>['0', '1'];
+        await picker.getMultiImageWithOptions(
+          options: const MultiImagePickerOptions(
+            imageOptions: ImageOptions(requestFullMetadata: false),
+          ),
+        );
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('pickMultiImage', arguments: <String, dynamic>{
+              'maxWidth': null,
+              'maxHeight': null,
+              'imageQuality': null,
+              'requestFullMetadata': false,
+            }),
+          ],
+        );
+      });
+    });
   });
 }