diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index f93b6ec181..bcba89bd39 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.8.0 + +* Adds `getMedia` method. + ## 2.7.0 * Adds `CameraDelegatingImagePickerPlatform` as a base class for platform 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 c2c39f93fe..b21fd29a8d 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 @@ -252,6 +252,30 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final ImageOptions imageOptions = options.imageOptions; + + final Map args = { + 'maxImageWidth': imageOptions.maxWidth, + 'maxImageHeight': imageOptions.maxHeight, + 'imageQuality': imageOptions.imageQuality, + 'allowMultiple': options.allowMultiple, + }; + + final List? paths = await _channel + .invokeMethod?>( + 'pickMedia', + args, + ) + .then((List? paths) => + paths?.map((dynamic path) => XFile(path as String)).toList()); + + return paths ?? []; + } + @override Future getVideo({ required ImageSource source, @@ -280,13 +304,21 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') != result.containsKey('errorCode')); final String? type = result['type'] as String?; - assert(type == kTypeImage || type == kTypeVideo); + assert( + type == kTypeImage || type == kTypeVideo || type == kTypeMedia, + ); RetrieveType? retrieveType; - if (type == kTypeImage) { - retrieveType = RetrieveType.image; - } else if (type == kTypeVideo) { - retrieveType = RetrieveType.video; + switch (type) { + case kTypeImage: + retrieveType = RetrieveType.image; + break; + case kTypeVideo: + retrieveType = RetrieveType.video; + break; + case kTypeMedia: + retrieveType = RetrieveType.media; + break; } PlatformException? exception; 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 e01caca146..66c5d3b578 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 @@ -213,6 +213,24 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('getMultiImage() has not been implemented.'); } + /// Returns a [List] with the images and/or videos that were picked. + /// The images and videos come from the 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. + /// + /// 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. + /// + /// If no images or videos were picked, the return value is an empty list. + Future> getMedia({ + required MediaOptions options, + }) { + throw UnimplementedError('getMedia() has not been implemented.'); + } + /// Returns a [XFile] 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/lib/src/types/image_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart index 2cc01c92da..374ff27063 100644 --- 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 @@ -2,6 +2,40 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'types.dart'; + +/// Specifies options for picking a single image from the device's camera or gallery. +/// +/// This class inheritance is a byproduct of the api changing over time. +/// It exists solely to avoid breaking changes. +class ImagePickerOptions extends ImageOptions { + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], + /// [referredCameraDevice] and [requestFullMetadata]. + const ImagePickerOptions({ + super.maxHeight, + super.maxWidth, + super.imageQuality, + super.requestFullMetadata, + this.preferredCameraDevice = CameraDevice.rear, + }) : super(); + + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], + /// [referredCameraDevice] and [requestFullMetadata]. + ImagePickerOptions.createAndValidate({ + super.maxHeight, + super.maxWidth, + super.imageQuality, + super.requestFullMetadata, + this.preferredCameraDevice = CameraDevice.rear, + }) : super.createAndValidate(); + + /// Used to specify the camera to use when the `source` is [ImageSource.camera]. + /// + /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not + /// supported on the device. Defaults to [CameraDevice.rear]. + final CameraDevice preferredCameraDevice; +} + /// Specifies image-specific options for picking. class ImageOptions { /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality] @@ -13,6 +47,18 @@ class ImageOptions { this.requestFullMetadata = true, }); + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality] + /// and [requestFullMetadata]. Throws if options are not valid. + ImageOptions.createAndValidate({ + this.maxHeight, + this.maxWidth, + this.imageQuality, + this.requestFullMetadata = true, + }) { + _validateOptions( + maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality); + } + /// The maximum width of the image, in pixels. /// /// If null, the image will only be resized if [maxHeight] is specified. @@ -38,4 +84,19 @@ class ImageOptions { // // Defaults to true. final bool requestFullMetadata; + + /// Validates that all values are within required ranges. Throws if not. + static void _validateOptions( + {double? maxWidth, final 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'); + } + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart deleted file mode 100644 index 0d85c918f6..0000000000 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart +++ /dev/null @@ -1,50 +0,0 @@ -// 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 'types.dart'; - -/// Specifies options for picking a single image from the device's camera or gallery. -class ImagePickerOptions { - /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], - /// [referredCameraDevice] and [requestFullMetadata]. - const ImagePickerOptions({ - this.maxHeight, - this.maxWidth, - this.imageQuality, - this.preferredCameraDevice = CameraDevice.rear, - 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; - - /// Used to specify the camera to use when the `source` is [ImageSource.camera]. - /// - /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not - /// supported on the device. Defaults to [CameraDevice.rear]. - final CameraDevice preferredCameraDevice; - - /// 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/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart index 10af812a31..0f802f1971 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart @@ -36,7 +36,8 @@ class LostDataResponse { /// An empty response should have [file], [exception] and [type] to be null. bool get isEmpty => _empty; - /// The file that was lost in a previous [getImage], [getMultiImage] or [getVideo] call due to MainActivity being destroyed. + /// The file that was lost in a previous [getImage], [getMultiImage], + /// [getVideo] or [getMedia] call due to MainActivity being destroyed. /// /// Can be null if [exception] exists. final XFile? file; @@ -51,7 +52,7 @@ class LostDataResponse { /// Note that it is not the exception that caused the destruction of the MainActivity. final PlatformException? exception; - /// Can either be [RetrieveType.image] or [RetrieveType.video]; + /// Can either be [RetrieveType.image], [RetrieveType.video], or [RetrieveType.media]. /// /// If the lost data is empty, this will be null. final RetrieveType? type; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart new file mode 100644 index 0000000000..70a048f714 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart @@ -0,0 +1,23 @@ +// 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:flutter/foundation.dart'; + +import '../../image_picker_platform_interface.dart'; + +/// Specifies options for selecting items when using [ImagePickerPlatform.getMedia]. +@immutable +class MediaOptions { + /// Construct a new MediaOptions instance. + const MediaOptions({ + this.imageOptions = const ImageOptions(), + required this.allowMultiple, + }); + + /// Options that will apply to images upon selection. + final ImageOptions imageOptions; + + /// Whether to allow for selecting multiple media. + final bool allowMultiple; +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart new file mode 100644 index 0000000000..cd0113497e --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart @@ -0,0 +1,14 @@ +// 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 '../../image_picker_platform_interface.dart'; + +/// The type of media to allow the user to select with [ImagePickerPlatform.getMedia]. +enum MediaSelectionType { + /// Static pictures. + image, + + /// Videos. + video, +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart index 445445e5d7..94fed59f23 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart @@ -8,5 +8,8 @@ enum RetrieveType { image, /// A video. See [ImagePicker.pickVideo]. - video + video, + + /// Either a video or a static picture. See [ImagePicker.pickMedia]. + media, } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index fcb76ccefa..0339d98b57 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -5,9 +5,10 @@ export 'camera_delegate.dart'; export 'camera_device.dart'; export 'image_options.dart'; -export 'image_picker_options.dart'; export 'image_source.dart'; export 'lost_data_response.dart'; +export 'media_options.dart'; +export 'media_selection_type.dart'; export 'multi_image_picker_options.dart'; export 'picked_file/picked_file.dart'; export 'retrieve_type.dart'; @@ -17,3 +18,6 @@ const String kTypeImage = 'image'; /// Denotes that a video is being picked. const String kTypeVideo = 'video'; + +/// Denotes that either a video or image is being picked. +const String kTypeMedia = 'media'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 3f1e523453..67a5070f6c 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/packages/tree/main/packages/image_picker/ 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.7.0 +version: 2.8.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart index 244af39826..cf92c2cfa1 100644 --- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart @@ -872,6 +872,152 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + returnValue = ['0']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + + expect( + log, + [ + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + ], + ); + }); + + test('passes the selection options correctly', () async { + // Default options + returnValue = ['0']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + // Various image options + returnValue = ['0']; + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + ), + ), + ); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + ), + ), + ); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: 70, + ), + ), + ); + + expect( + log, + [ + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': 10.0, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': 10.0, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': 70, + 'allowMultiple': true, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: -1.0, + ), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: -1.0, + ), + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: -1, + ), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: 101, + ), + ), + ), + throwsArgumentError, + ); + }); + + test('handles a null path response gracefully', () async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); + expect( + await picker.getMedia( + options: const MediaOptions(allowMultiple: true)), + []); + }); + }); + group('#getVideo', () { test('passes the image source argument correctly', () async { await picker.getVideo(source: ImageSource.camera);