diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 0e50cd22ec..f93b6ec181 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.7.0 + +* Adds `CameraDelegatingImagePickerPlatform` as a base class for platform + implementations that don't support `ImageSource.camera`, but allow for an- + implementation to be provided at the application level via implementation + of `CameraDelegatingImagePickerPlatform`. +* Adds `supportsImageSource` to check source support at runtime. + ## 2.6.4 * Adds compatibility with `http` 1.0. @@ -32,7 +40,7 @@ * 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. +* Minor fixes for new analysis options. ## 2.4.4 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 c8942cd2da..e01caca146 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 @@ -32,8 +32,6 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [ImagePickerPlatform] when they register themselves. - // TODO(amirh): Extract common platform interface logic. - // https://github.com/flutter/flutter/issues/43368 static set instance(ImagePickerPlatform instance) { PlatformInterface.verify(instance, _token); _instance = instance; @@ -305,4 +303,75 @@ abstract class ImagePickerPlatform extends PlatformInterface { ); return pickedImages ?? []; } + + /// Returns true if the implementation supports [source]. + /// + /// Defaults to true for the original image sources, `gallery` and `camera`, + /// for backwards compatibility. + bool supportsImageSource(ImageSource source) { + return source == ImageSource.gallery || source == ImageSource.camera; + } +} + +/// A base class for an [ImagePickerPlatform] implementation that does not +/// directly support [ImageSource.camera], but supports delegating to a +/// provided [ImagePickerCameraDelegate]. +abstract class CameraDelegatingImagePickerPlatform extends ImagePickerPlatform { + /// A delegate to respond to calls that use [ImageSource.camera]. + /// + /// When it is null, attempting to use [ImageSource.camera] will throw a + /// [StateError]. + ImagePickerCameraDelegate? cameraDelegate; + + @override + bool supportsImageSource(ImageSource source) { + if (source == ImageSource.camera) { + return cameraDelegate != null; + } + return super.supportsImageSource(source); + } + + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + if (source == ImageSource.camera) { + final ImagePickerCameraDelegate? delegate = cameraDelegate; + if (delegate == null) { + throw StateError( + 'This implementation of ImagePickerPlatform requires a ' + '"cameraDelegate" in order to use ImageSource.camera'); + } + return delegate.takePhoto( + options: ImagePickerCameraDelegateOptions( + preferredCameraDevice: options.preferredCameraDevice, + )); + } + return super.getImageFromSource(source: source, options: options); + } + + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + if (source == ImageSource.camera) { + final ImagePickerCameraDelegate? delegate = cameraDelegate; + if (delegate == null) { + throw StateError( + 'This implementation of ImagePickerPlatform requires a ' + '"cameraDelegate" in order to use ImageSource.camera'); + } + return delegate.takeVideo( + options: ImagePickerCameraDelegateOptions( + preferredCameraDevice: preferredCameraDevice, + maxVideoDuration: maxDuration)); + } + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart new file mode 100644 index 0000000000..39584c923b --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart @@ -0,0 +1,53 @@ +// 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:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart' show immutable; + +import 'camera_device.dart'; + +/// Options for [ImagePickerCameraDelegate] methods. +/// +/// New options may be added in the future. +@immutable +class ImagePickerCameraDelegateOptions { + /// Creates a new set of options for taking an image or video. + const ImagePickerCameraDelegateOptions({ + this.preferredCameraDevice = CameraDevice.rear, + this.maxVideoDuration, + }); + + /// The camera device to default to, if available. + /// + /// Defaults to [CameraDevice.rear]. + final CameraDevice preferredCameraDevice; + + /// The maximum duration to allow when recording a video. + /// + /// Defaults to null, meaning no maximum duration. + final Duration? maxVideoDuration; +} + +/// A delegate for `ImagePickerPlatform` implementations that do not provide +/// a camera implementation, or that have a default but allow substituting an +/// alternate implementation. +abstract class ImagePickerCameraDelegate { + /// Takes a photo with the given [options] and returns an [XFile] to the + /// resulting image file. + /// + /// Returns null if the photo could not be taken, or the user cancelled. + Future takePhoto({ + ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions(), + }); + + /// Records a video with the given [options] and returns an [XFile] to the + /// resulting video file. + /// + /// Returns null if the video could not be recorded, or the user cancelled. + Future takeVideo({ + ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions(), + }); +} 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 fbe12e8e82..fcb76ccefa 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 @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'camera_delegate.dart'; export 'camera_device.dart'; export 'image_options.dart'; export 'image_picker_options.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 9c8a55ad96..3f1e523453 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.6.4 +version: 2.7.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart b/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart new file mode 100644 index 0000000000..89dc1ae382 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart @@ -0,0 +1,104 @@ +// 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_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +void main() { + group('ImagePickerPlatform', () { + test('supportsImageSource defaults to true for original values', () async { + final ImagePickerPlatform implementation = FakeImagePickerPlatform(); + + expect(implementation.supportsImageSource(ImageSource.camera), true); + expect(implementation.supportsImageSource(ImageSource.gallery), true); + }); + }); + + group('CameraDelegatingImagePickerPlatform', () { + test( + 'supportsImageSource returns false for camera when there is no delegate', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + expect(implementation.supportsImageSource(ImageSource.camera), false); + }); + + test('supportsImageSource returns true for camera when there is a delegate', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = FakeCameraDelegate(); + + expect(implementation.supportsImageSource(ImageSource.camera), true); + }); + + test('getImageFromSource for camera throws if delegate is not set', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + await expectLater( + implementation.getImageFromSource(source: ImageSource.camera), + throwsStateError); + }); + + test('getVideo for camera throws if delegate is not set', () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + await expectLater(implementation.getVideo(source: ImageSource.camera), + throwsStateError); + }); + + test('getImageFromSource for camera calls delegate if set', () async { + const String fakePath = '/tmp/foo'; + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = + FakeCameraDelegate(result: XFile(fakePath)); + + expect( + (await implementation.getImageFromSource(source: ImageSource.camera))! + .path, + fakePath); + }); + + test('getVideo for camera calls delegate if set', () async { + const String fakePath = '/tmp/foo'; + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = + FakeCameraDelegate(result: XFile(fakePath)); + + expect((await implementation.getVideo(source: ImageSource.camera))!.path, + fakePath); + }); + }); +} + +class FakeImagePickerPlatform extends ImagePickerPlatform {} + +class FakeCameraDelegatingImagePickerPlatform + extends CameraDelegatingImagePickerPlatform {} + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} 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/method_channel_image_picker_test.dart similarity index 100% rename from packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart rename to packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart