diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 2defdd2d84cc..0a4e98bf7dbe 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,6 +1,10 @@ -## NEXT +## 2.5.0 -* Minor fixes for new analysis options. +* Deprecates `getImage` in favor of a new method `getImageFromSource`. + * Adds `requestFullMetadata` option that allows disabling extra permission requests + on certain platforms. + * Moves optional image picking parameters to `ImagePickerOptions` class. +* Minor fixes for new analysis options. ## 2.4.4 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 e1e6082c8047..ba5d60d7a677 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 @@ -87,6 +87,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxHeight, int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( @@ -108,7 +109,8 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxWidth': maxWidth, 'maxHeight': maxHeight, 'imageQuality': imageQuality, - 'cameraDevice': preferredCameraDevice.index + 'cameraDevice': preferredCameraDevice.index, + 'requestFullMetadata': requestFullMetadata, }, ); } @@ -197,6 +199,22 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return path != null ? XFile(path) : null; } + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + final String? path = await _getImagePath( + source: source, + maxHeight: options.maxHeight, + maxWidth: options.maxWidth, + imageQuality: options.imageQuality, + preferredCameraDevice: options.preferredCameraDevice, + requestFullMetadata: options.requestFullMetadata, + ); + return path != null ? XFile(path) : null; + } + @override Future?> getMultiImage({ double? maxWidth, 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 8f02e1683267..d1d06f904fe6 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 @@ -146,6 +146,8 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('retrieveLostData() has not been implemented.'); } + /// This method is deprecated in favor of [getImageFromSource] and will be removed in a future update. + /// /// Returns an [XFile] with the image that was picked. /// /// The `source` argument controls where the image comes from. This can @@ -251,4 +253,34 @@ abstract class ImagePickerPlatform extends PlatformInterface { Future getLostData() { throw UnimplementedError('getLostData() has not been implemented.'); } + + /// Returns an [XFile] with the image that was picked. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// The `options` argument controls additional settings that can be used when + /// picking an image. See [ImagePickerOptions] for more details. + /// + /// 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 in [ImagePickerOptions]. + /// + /// 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 were picked, the return value is null. + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) { + return getImage( + source: source, + maxHeight: options.maxHeight, + maxWidth: options.maxWidth, + imageQuality: options.imageQuality, + preferredCameraDevice: options.preferredCameraDevice, + ); + } } 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 new file mode 100644 index 000000000000..cdc89a920178 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart @@ -0,0 +1,50 @@ +// 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/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/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index 7f2844230287..dad86c5d1ba1 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 @@ -3,6 +3,7 @@ // found in the LICENSE file. export 'camera_device.dart'; +export 'image_picker_options.dart'; export 'image_source.dart'; export 'lost_data_response.dart'; export 'picked_file/picked_file.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 54fd17e47260..be6d5442d03b 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.4.4 +version: 2.5.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 79d971f217f0..72ed363ef7ae 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 @@ -40,14 +40,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -93,55 +95,62 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); }); - test('does not accept a invalid imageQuality argument', () { + test('does not accept an invalid imageQuality argument', () { expect( () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery), throwsArgumentError, @@ -196,6 +205,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -215,6 +225,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'requestFullMetadata': true, }), ], ); @@ -320,7 +331,7 @@ void main() { ); }); - test('does not accept a invalid imageQuality argument', () { + test('does not accept an invalid imageQuality argument', () { returnValue = ['0', '1']; expect( () => picker.pickMultiImage(imageQuality: -1), @@ -509,14 +520,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -562,55 +575,62 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); }); - test('does not accept a invalid imageQuality argument', () { + test('does not accept an invalid imageQuality argument', () { expect( () => picker.getImage(imageQuality: -1, source: ImageSource.gallery), throwsArgumentError, @@ -664,6 +684,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -683,6 +704,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'requestFullMetadata': true, }), ], ); @@ -788,7 +810,7 @@ void main() { ); }); - test('does not accept a invalid imageQuality argument', () { + test('does not accept an invalid imageQuality argument', () { returnValue = ['0', '1']; expect( () => picker.getMultiImage(imageQuality: -1), @@ -979,5 +1001,261 @@ void main() { expect(picker.getLostData(), throwsAssertionError); }); }); + + group('#getImageFromSource', () { + test('passes the image source argument correctly', () async { + await picker.getImageFromSource(source: ImageSource.camera); + await picker.getImageFromSource(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImageFromSource(source: ImageSource.camera); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxWidth: 10.0), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxHeight: 10.0), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + maxHeight: 20.0, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + imageQuality: 70, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxHeight: 10.0, + imageQuality: 70, + ), + ); + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('does not accept an invalid imageQuality argument', () { + expect( + () => picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(imageQuality: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.gallery, + options: const ImagePickerOptions(imageQuality: 101), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: -1), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(imageQuality: 101), + ), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxWidth: -1.0), + ), + throwsArgumentError, + ); + + expect( + () => picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(maxHeight: -1.0), + ), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getImageFromSource(source: ImageSource.gallery), + isNull); + expect(await picker.getImageFromSource(source: ImageSource.camera), + isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImageFromSource(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions( + preferredCameraDevice: CameraDevice.front, + ), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + 'requestFullMetadata': true, + }), + ], + ); + }); + + test('passes the full metadata argument correctly', () async { + await picker.getImageFromSource( + source: ImageSource.camera, + options: const ImagePickerOptions(requestFullMetadata: false), + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + 'requestFullMetadata': false, + }), + ], + ); + }); + }); }); }