From 278bc6668567adb5c20dc17a71f03ef404b0d8c8 Mon Sep 17 00:00:00 2001 From: Piotr Mitkowski Date: Tue, 26 Jul 2022 16:17:04 +0200 Subject: [PATCH] [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 --- .../CHANGELOG.md | 7 + .../method_channel_image_picker.dart | 20 ++ .../image_picker_platform.dart | 22 ++ .../lib/src/types/image_options.dart | 41 ++++ .../src/types/multi_image_picker_options.dart | 16 ++ .../pubspec.yaml | 2 +- .../new_method_channel_image_picker_test.dart | 224 ++++++++++++++++++ 7 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/multi_image_picker_options.dart diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 0a4e98bf7dbe..120b7b00ed15 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 ba5d60d7a677..d215fa2684ee 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/foundation.dart'; 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 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool requestFullMetadata = true, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( @@ -77,6 +79,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxWidth': maxWidth, 'maxHeight': maxHeight, 'imageQuality': imageQuality, + 'requestFullMetadata': requestFullMetadata, }, ); } @@ -233,6 +236,23 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMultiImageWithOptions({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + final List? paths = await _getMultiImagePath( + maxWidth: options.imageOptions.maxWidth, + maxHeight: options.imageOptions.maxHeight, + imageQuality: options.imageOptions.imageQuality, + requestFullMetadata: options.imageOptions.requestFullMetadata, + ); + if (paths == null) { + return []; + } + + return paths.map((dynamic path) => XFile(path as String)).toList(); + } + @override Future getVideo({ required ImageSource source, 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 d1d06f904fe6..a2618d5b419c 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 'dart:async'; 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 @@ abstract class ImagePickerPlatform extends PlatformInterface { 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] with the images that were picked. /// /// The images come from the [ImageSource.gallery]. @@ -283,4 +286,23 @@ abstract class ImagePickerPlatform extends PlatformInterface { preferredCameraDevice: options.preferredCameraDevice, ); } + + /// Returns a [List] 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> getMultiImageWithOptions({ + MultiImagePickerOptions options = const MultiImagePickerOptions(), + }) async { + final List? pickedImages = await getMultiImage( + maxWidth: options.imageOptions.maxWidth, + maxHeight: options.imageOptions.maxHeight, + imageQuality: options.imageOptions.imageQuality, + ); + return pickedImages ?? []; + } } 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 000000000000..2cc01c92da1d --- /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 000000000000..4d7971c59a81 --- /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 4ce1d2fc52f1..50d84f81d888 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 @@ repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/i 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 72ed363ef7ae..27d7016d8143 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:flutter_test/flutter_test.dart'; 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 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), ], ); @@ -283,36 +286,43 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, + 'requestFullMetadata': true, }), ], ); @@ -723,6 +733,7 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), ], ); @@ -762,36 +773,43 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, + 'requestFullMetadata': true, }), isMethodCall('pickMultiImage', arguments: { 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, + 'requestFullMetadata': true, }), ], ); @@ -1257,5 +1275,211 @@ void main() { ); }); }); + + group('#getMultiImageWithOptions', () { + test('calls the method correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImageWithOptions(); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width, height and imageQuality arguments correctly', + () async { + returnValue = ['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, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['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 = ['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 = ['0', '1']; + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions(), + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the request full metadata argument correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImageWithOptions( + options: const MultiImagePickerOptions( + imageOptions: ImageOptions(requestFullMetadata: false), + ), + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': false, + }), + ], + ); + }); + }); }); }