diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..75d169ea1d6a --- /dev/null +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial open-source release diff --git a/packages/camera/camera_platform_interface/LICENSE b/packages/camera/camera_platform_interface/LICENSE new file mode 100644 index 000000000000..a6d6c0749818 --- /dev/null +++ b/packages/camera/camera_platform_interface/LICENSE @@ -0,0 +1,25 @@ +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/camera/camera_platform_interface/README.md b/packages/camera/camera_platform_interface/README.md new file mode 100644 index 000000000000..43be651935b5 --- /dev/null +++ b/packages/camera/camera_platform_interface/README.md @@ -0,0 +1,26 @@ +# camera_platform_interface + +A common platform interface for the [`camera`][1] plugin. + +This interface allows platform-specific implementations of the `camera` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `camera`, extend +[`CameraPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`CameraPlatform` by calling +`CameraPlatform.instance = MyPlatformCamera()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../camera +[2]: lib/camera_platform_interface.dart diff --git a/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart new file mode 100644 index 000000000000..eaa14da45a5e --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart @@ -0,0 +1,10 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/events/camera_event.dart'; +export 'src/platform_interface/camera_platform.dart'; +export 'src/types/types.dart'; + +/// Expose XFile +export 'package:cross_file/cross_file.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart new file mode 100644 index 000000000000..ab3d45545f23 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -0,0 +1,198 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Generic Event coming from the native side of Camera. +/// +/// All [CameraEvent]s contain the `cameraId` that originated the event. This +/// should never be `null`. +/// +/// This class is used as a base class for all the events that might be +/// triggered from a Camera, but it is never used directly as an event type. +/// +/// Do NOT instantiate new events like `CameraEvent(cameraId)` directly, +/// use a specific class instead: +/// +/// Do `class NewEvent extend CameraEvent` when creating your own events. +/// See below for examples: `CameraClosingEvent`, `CameraErrorEvent`... +/// These events are more semantic and more pleasant to use than raw generics. +/// They can be (and in fact, are) filtered by the `instanceof`-operator. +abstract class CameraEvent { + /// The ID of the Camera this event is associated to. + final int cameraId; + + /// Build a Camera Event, that relates a `cameraId`. + /// + /// The `cameraId` is the ID of the camera that triggered the event. + CameraEvent(this.cameraId) : assert(cameraId != null); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraEvent && + runtimeType == other.runtimeType && + cameraId == other.cameraId; + + @override + int get hashCode => cameraId.hashCode; +} + +/// An event fired when the camera has finished initializing. +class CameraInitializedEvent extends CameraEvent { + /// The width of the preview in pixels. + final double previewWidth; + + /// The height of the preview in pixels. + final double previewHeight; + + /// Build a CameraInitialized event triggered from the camera represented by + /// `cameraId`. + /// + /// The `previewWidth` represents the width of the generated preview in pixels. + /// The `previewHeight` represents the height of the generated preview in pixels. + CameraInitializedEvent( + int cameraId, + this.previewWidth, + this.previewHeight, + ) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraInitializedEvent] + /// class. + CameraInitializedEvent.fromJson(Map json) + : previewWidth = json['previewWidth'], + previewHeight = json['previewHeight'], + super(json['cameraId']); + + /// Converts the [CameraInitializedEvent] instance into a [Map] instance that + /// can be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'previewWidth': previewWidth, + 'previewHeight': previewHeight, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is CameraInitializedEvent && + runtimeType == other.runtimeType && + previewWidth == other.previewWidth && + previewHeight == other.previewHeight; + + @override + int get hashCode => + super.hashCode ^ previewWidth.hashCode ^ previewHeight.hashCode; +} + +/// An event fired when the resolution preset of the camera has changed. +class CameraResolutionChangedEvent extends CameraEvent { + /// The capture width in pixels. + final double captureWidth; + + /// The capture height in pixels. + final double captureHeight; + + /// Build a CameraResolutionChanged event triggered from the camera + /// represented by `cameraId`. + /// + /// The `captureWidth` represents the width of the resulting image in pixels. + /// The `captureHeight` represents the height of the resulting image in pixels. + CameraResolutionChangedEvent( + int cameraId, + this.captureWidth, + this.captureHeight, + ) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the + /// [CameraResolutionChangedEvent] class. + CameraResolutionChangedEvent.fromJson(Map json) + : captureWidth = json['captureWidth'], + captureHeight = json['captureHeight'], + super(json['cameraId']); + + /// Converts the [CameraResolutionChangedEvent] instance into a [Map] instance + /// that can be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'captureWidth': captureWidth, + 'captureHeight': captureHeight, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraResolutionChangedEvent && + super == (other) && + runtimeType == other.runtimeType && + captureWidth == other.captureWidth && + captureHeight == other.captureHeight; + + @override + int get hashCode => + super.hashCode ^ captureWidth.hashCode ^ captureHeight.hashCode; +} + +/// An event fired when the camera is going to close. +class CameraClosingEvent extends CameraEvent { + /// Build a CameraClosing event triggered from the camera represented by + /// `cameraId`. + CameraClosingEvent(int cameraId) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraClosingEvent] + /// class. + CameraClosingEvent.fromJson(Map json) + : super(json['cameraId']); + + /// Converts the [CameraClosingEvent] instance into a [Map] instance that can + /// be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == (other) && + other is CameraClosingEvent && + runtimeType == other.runtimeType; + + @override + int get hashCode => super.hashCode; +} + +/// An event fired when an error occured while operating the camera. +class CameraErrorEvent extends CameraEvent { + /// Description of the error. + final String description; + + /// Build a CameraError event triggered from the camera represented by + /// `cameraId`. + /// + /// The `description` represents the error occured on the camera. + CameraErrorEvent(int cameraId, this.description) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraErrorEvent] + /// class. + CameraErrorEvent.fromJson(Map json) + : description = json['description'], + super(json['cameraId']); + + /// Converts the [CameraErrorEvent] instance into a [Map] instance that can be + /// serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'description': description, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == (other) && + other is CameraErrorEvent && + runtimeType == other.runtimeType && + description == other.description; + + @override + int get hashCode => super.hashCode ^ description.hashCode; +} diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart new file mode 100644 index 000000000000..c649a41f5e9f --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -0,0 +1,239 @@ +// Copyright 2018 The Chromium 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 'dart:async'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; +import 'package:stream_transform/stream_transform.dart'; + +const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera'); + +/// An implementation of [CameraPlatform] that uses method channels. +class MethodChannelCamera extends CameraPlatform { + final Map _channels = {}; + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController cameraEventStreamController = + StreamController.broadcast(); + + Stream _events(int cameraId) => + cameraEventStreamController.stream + .where((event) => event.cameraId == cameraId); + + @override + Future> availableCameras() async { + try { + final List> cameras = await _channel + .invokeListMethod>('availableCameras'); + return cameras.map((Map camera) { + return CameraDescription( + name: camera['name'], + lensDirection: parseCameraLensDirection(camera['lensFacing']), + sensorOrientation: camera['sensorOrientation'], + ); + }).toList(); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset resolutionPreset, { + bool enableAudio, + }) async { + try { + final Map reply = + await _channel.invokeMapMethod( + 'create', + { + 'cameraName': cameraDescription.name, + 'resolutionPreset': resolutionPreset != null + ? _serializeResolutionPreset(resolutionPreset) + : null, + 'enableAudio': enableAudio, + }, + ); + return reply['cameraId']; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future initializeCamera(int cameraId) { + _channels.putIfAbsent(cameraId, () { + final channel = MethodChannel('flutter.io/cameraPlugin/camera$cameraId'); + channel.setMethodCallHandler( + (MethodCall call) => handleMethodCall(call, cameraId)); + return channel; + }); + + Completer _completer = Completer(); + + onCameraInitialized(cameraId).first.then((value) { + _completer.complete(); + }); + + _channel.invokeMapMethod( + 'initialize', + { + 'cameraId': cameraId, + }, + ); + + return _completer.future; + } + + @override + Future dispose(int cameraId) async { + await _channel.invokeMethod( + 'dispose', + {'cameraId': cameraId}, + ); + + if (_channels.containsKey(cameraId)) { + _channels[cameraId].setMethodCallHandler(null); + _channels.remove(cameraId); + } + } + + @override + Stream onCameraInitialized(int cameraId) { + return _events(cameraId).whereType(); + } + + @override + Stream onCameraResolutionChanged(int cameraId) { + return _events(cameraId).whereType(); + } + + @override + Stream onCameraClosing(int cameraId) { + return _events(cameraId).whereType(); + } + + @override + Stream onCameraError(int cameraId) { + return _events(cameraId).whereType(); + } + + @override + Future takePicture(int cameraId) async { + String path = await _channel.invokeMethod( + 'takePicture', + {'cameraId': cameraId}, + ); + return XFile(path); + } + + @override + Future prepareForVideoRecording() => + _channel.invokeMethod('prepareForVideoRecording'); + + @override + Future startVideoRecording(int cameraId) async { + await _channel.invokeMethod( + 'startVideoRecording', + {'cameraId': cameraId}, + ); + } + + @override + Future stopVideoRecording(int cameraId) async { + String path = await _channel.invokeMethod( + 'stopVideoRecording', + {'cameraId': cameraId}, + ); + return XFile(path); + } + + @override + Future pauseVideoRecording(int cameraId) => _channel.invokeMethod( + 'pauseVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Future resumeVideoRecording(int cameraId) => + _channel.invokeMethod( + 'resumeVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Widget buildPreview(int cameraId) { + return Texture(textureId: cameraId); + } + + /// Returns the resolution preset as a String. + String _serializeResolutionPreset(ResolutionPreset resolutionPreset) { + switch (resolutionPreset) { + case ResolutionPreset.max: + return 'max'; + case ResolutionPreset.ultraHigh: + return 'ultraHigh'; + case ResolutionPreset.veryHigh: + return 'veryHigh'; + case ResolutionPreset.high: + return 'high'; + case ResolutionPreset.medium: + return 'medium'; + case ResolutionPreset.low: + return 'low'; + default: + throw ArgumentError('Unknown ResolutionPreset value'); + } + } + + /// Converts messages received from the native platform into events. + /// + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + Future handleMethodCall(MethodCall call, int cameraId) async { + switch (call.method) { + case 'initialized': + cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + call.arguments['previewWidth'], + call.arguments['previewHeight'], + )); + break; + case 'resolution_changed': + cameraEventStreamController.add(CameraResolutionChangedEvent( + cameraId, + call.arguments['captureWidth'], + call.arguments['captureHeight'], + )); + break; + case 'camera_closing': + cameraEventStreamController.add(CameraClosingEvent( + cameraId, + )); + break; + case 'error': + cameraEventStreamController.add(CameraErrorEvent( + cameraId, + call.arguments['description'], + )); + break; + default: + throw MissingPluginException(); + } + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart new file mode 100644 index 000000000000..5281a423459a --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -0,0 +1,120 @@ +// Copyright 2018 The Chromium 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 'dart:async'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/widgets.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// The interface that implementations of camera must implement. +/// +/// Platform implementations should extend this class rather than implement it as `camera` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [CameraPlatform] methods. +abstract class CameraPlatform extends PlatformInterface { + /// Constructs a CameraPlatform. + CameraPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CameraPlatform _instance = MethodChannelCamera(); + + /// The default instance of [CameraPlatform] to use. + /// + /// Defaults to [MethodChannelCamera]. + static CameraPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [CameraPlatform] when they register themselves. + static set instance(CameraPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Completes with a list of available cameras. + Future> availableCameras() { + throw UnimplementedError('availableCameras() is not implemented.'); + } + + /// Creates an uninitialized camera instance and returns the cameraId. + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset resolutionPreset, { + bool enableAudio, + }) { + throw UnimplementedError('createCamera() is not implemented.'); + } + + /// Initializes the camera on the device. + Future initializeCamera(int cameraId) { + throw UnimplementedError('initializeCamera() is not implemented.'); + } + + /// The camera has been initialized + Stream onCameraInitialized(int cameraId) { + throw UnimplementedError('onCameraInitialized() is not implemented.'); + } + + /// The camera's resolution has changed + Stream onCameraResolutionChanged(int cameraId) { + throw UnimplementedError('onResolutionChanged() is not implemented.'); + } + + /// The camera started to close. + Stream onCameraClosing(int cameraId) { + throw UnimplementedError('onCameraClosing() is not implemented.'); + } + + /// The camera experienced an error. + Stream onCameraError(int cameraId) { + throw UnimplementedError('onCameraError() is not implemented.'); + } + + /// Captures an image and returns the file where it was saved. + Future takePicture(int cameraId) { + throw UnimplementedError('takePicture() is not implemented.'); + } + + /// Prepare the capture session for video recording. + Future prepareForVideoRecording() { + throw UnimplementedError('prepareForVideoRecording() is not implemented.'); + } + + /// Starts a video recording. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + Future startVideoRecording(int cameraId) { + throw UnimplementedError('startVideoRecording() is not implemented.'); + } + + /// Stops the video recording and returns the file where it was saved. + Future stopVideoRecording(int cameraId) { + throw UnimplementedError('stopVideoRecording() is not implemented.'); + } + + /// Pause video recording. + Future pauseVideoRecording(int cameraId) { + throw UnimplementedError('pauseVideoRecording() is not implemented.'); + } + + /// Resume video recording after pausing. + Future resumeVideoRecording(int cameraId) { + throw UnimplementedError('resumeVideoRecording() is not implemented.'); + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview(int cameraId) { + throw UnimplementedError('buildView() has not been implemented.'); + } + + /// Releases the resources of this camera. + Future dispose(int cameraId) { + throw UnimplementedError('dispose() is not implemented.'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart new file mode 100644 index 000000000000..c19af1f50d1c --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart @@ -0,0 +1,52 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The direction the camera is facing. +enum CameraLensDirection { + /// Front facing camera (a user looking at the screen is seen by the camera). + front, + + /// Back facing camera (a user looking at the screen is not seen by the camera). + back, + + /// External camera which may not be mounted to the device. + external, +} + +/// Properties of a camera device. +class CameraDescription { + /// Creates a new camera description with the given properties. + CameraDescription({this.name, this.lensDirection, this.sensorOrientation}); + + /// The name of the camera device. + final String name; + + /// The direction the camera is facing. + final CameraLensDirection lensDirection; + + /// Clockwise angle through which the output image needs to be rotated to be upright on the device screen in its native orientation. + /// + /// **Range of valid values:** + /// 0, 90, 180, 270 + /// + /// On Android, also defines the direction of rolling shutter readout, which + /// is from top to bottom in the sensor's coordinate system. + final int sensorOrientation; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraDescription && + runtimeType == other.runtimeType && + name == other.name && + lensDirection == other.lensDirection; + + @override + int get hashCode => name.hashCode ^ lensDirection.hashCode; + + @override + String toString() { + return '$runtimeType($name, $lensDirection, $sensorOrientation)'; + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart new file mode 100644 index 000000000000..3da659f7021d --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart @@ -0,0 +1,20 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// This is thrown when the plugin reports an error. +class CameraException implements Exception { + /// Creates a new camera exception with the given error code and description. + CameraException(this.code, this.description); + + /// Error code. + // TODO(bparrishMines): Document possible error codes. + // https://github.com/flutter/flutter/issues/69298 + String code; + + /// Textual description of the error. + String description; + + @override + String toString() => 'CameraException($code, $description)'; +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart new file mode 100644 index 000000000000..ead592364131 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart @@ -0,0 +1,26 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Affect the quality of video recording and image capture: +/// +/// If a preset is not available on the camera being used a preset of lower quality will be selected automatically. +enum ResolutionPreset { + /// 352x288 on iOS, 240p (320x240) on Android + low, + + /// 480p (640x480 on iOS, 720x480 on Android) + medium, + + /// 720p (1280x720) + high, + + /// 1080p (1920x1080) + veryHigh, + + /// 2160p (3840x2160) + ultraHigh, + + /// The highest resolution available. + max, +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..71e7a97ef49a --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -0,0 +1,7 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'camera_description.dart'; +export 'resolution_preset.dart'; +export 'camera_exception.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart new file mode 100644 index 000000000000..f94d8e69c07e --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -0,0 +1,14 @@ +import 'package:camera_platform_interface/camera_platform_interface.dart'; + +/// Parses a string into a corresponding CameraLensDirection. +CameraLensDirection parseCameraLensDirection(String string) { + switch (string) { + case 'front': + return CameraLensDirection.front; + case 'back': + return CameraLensDirection.back; + case 'external': + return CameraLensDirection.external; + } + throw ArgumentError('Unknown CameraLensDirection value'); +} diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..c2bb6fcc5963 --- /dev/null +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -0,0 +1,25 @@ +name: camera_platform_interface +description: A common platform interface for the camera plugin. +homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface +# 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: 1.0.0 + +dependencies: + flutter: + sdk: flutter + meta: ^1.0.5 + plugin_platform_interface: ^1.0.1 + cross_file: ^0.1.0 + stream_transform: ^1.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + async: ^2.4.2 + mockito: ^4.1.1 + pedantic: ^1.8.0 + +environment: + sdk: ">=2.1.0 <3.0.0" + flutter: ">=1.22.0 <2.0.0" diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart new file mode 100644 index 000000000000..2bc35bf4055c --- /dev/null +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -0,0 +1,229 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$CameraPlatform', () { + test('$MethodChannelCamera is the default instance', () { + expect(CameraPlatform.instance, isA()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + CameraPlatform.instance = ImplementsCameraPlatform(); + }, throwsNoSuchMethodError); + }); + + test('Can be extended', () { + CameraPlatform.instance = ExtendsCameraPlatform(); + }); + + test('Can be mocked with `implements`', () { + final mock = MockCameraPlatform(); + CameraPlatform.instance = mock; + }); + + test( + 'Default implementation of availableCameras() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.availableCameras(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraInitialized() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraInitialized(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onResolutionChanged() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraResolutionChanged(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraClosing() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraClosing(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraError() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraError(1), + throwsUnimplementedError, + ); + }); + + test('Default implementation of dispose() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.dispose(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of createCamera() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.createCamera(null, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of initializeCamera() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.initializeCamera(null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of pauseVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.pauseVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of prepareForVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.prepareForVideoRecording(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of resumeVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.resumeVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of startVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.startVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of stopVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.stopVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of takePicture() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.takePicture(1), + throwsUnimplementedError, + ); + }); + }); +} + +class ImplementsCameraPlatform implements CameraPlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockCameraPlatform extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + CameraPlatform {} + +class ExtendsCameraPlatform extends CameraPlatform {} diff --git a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart new file mode 100644 index 000000000000..01b03b8e93a0 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart @@ -0,0 +1,252 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraInitializedEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraInitializedEvent(1, 1024, 640); + + expect(event.cameraId, 1); + expect(event.previewWidth, 1024); + expect(event.previewHeight, 640); + }); + + test('fromJson should initialize all properties', () { + final event = CameraInitializedEvent.fromJson({ + 'cameraId': 1, + 'previewWidth': 1024.0, + 'previewHeight': 640.0, + }); + + expect(event.cameraId, 1); + expect(event.previewWidth, 1024); + expect(event.previewHeight, 640); + }); + + test('toJson should return a map with all fields', () { + final event = CameraInitializedEvent(1, 1024, 640); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 3); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['previewWidth'], 1024); + expect(jsonMap['previewHeight'], 640); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraInitializedEvent(1, 1024, 640); + final secondEvent = CameraInitializedEvent(1, 1024, 640); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraInitializedEvent(1, 1024, 640); + final secondEvent = CameraInitializedEvent(2, 1024, 640); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if previewWidth is different', () { + final firstEvent = CameraInitializedEvent(1, 1024, 640); + final secondEvent = CameraInitializedEvent(1, 2048, 640); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if previewHeight is different', () { + final firstEvent = CameraInitializedEvent(1, 1024, 640); + final secondEvent = CameraInitializedEvent(1, 1024, 980); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraInitializedEvent(1, 1024, 640); + final expectedHashCode = event.cameraId.hashCode ^ + event.previewWidth.hashCode ^ + event.previewHeight.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraResolutionChangesEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + + expect(event.cameraId, 1); + expect(event.captureWidth, 1024); + expect(event.captureHeight, 640); + }); + + test('fromJson should initialize all properties', () { + final event = CameraResolutionChangedEvent.fromJson({ + 'cameraId': 1, + 'captureWidth': 1024.0, + 'captureHeight': 640.0, + }); + + expect(event.cameraId, 1); + expect(event.captureWidth, 1024); + expect(event.captureHeight, 640); + }); + + test('toJson should return a map with all fields', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 3); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['captureWidth'], 1024); + expect(jsonMap['captureHeight'], 640); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 1024, 640); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(2, 1024, 640); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if captureWidth is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 2048, 640); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if captureHeight is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 1024, 980); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + final expectedHashCode = event.cameraId.hashCode ^ + event.captureWidth.hashCode ^ + event.captureHeight.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraClosingEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraClosingEvent(1); + + expect(event.cameraId, 1); + }); + + test('fromJson should initialize all properties', () { + final event = CameraClosingEvent.fromJson({ + 'cameraId': 1, + }); + + expect(event.cameraId, 1); + }); + + test('toJson should return a map with all fields', () { + final event = CameraClosingEvent(1); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 1); + expect(jsonMap['cameraId'], 1); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraClosingEvent(1); + final secondEvent = CameraClosingEvent(1); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraClosingEvent(1); + final secondEvent = CameraClosingEvent(2); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraClosingEvent(1); + final expectedHashCode = event.cameraId.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraErrorEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraErrorEvent(1, 'Error'); + + expect(event.cameraId, 1); + expect(event.description, 'Error'); + }); + + test('fromJson should initialize all properties', () { + final event = CameraErrorEvent.fromJson( + {'cameraId': 1, 'description': 'Error'}); + + expect(event.cameraId, 1); + expect(event.description, 'Error'); + }); + + test('toJson should return a map with all fields', () { + final event = CameraErrorEvent(1, 'Error'); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 2); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['description'], 'Error'); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(1, 'Error'); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(2, 'Error'); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if description is different', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(1, 'Ooops'); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraErrorEvent(1, 'Error'); + final expectedHashCode = + event.cameraId.hashCode ^ event.description.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart new file mode 100644 index 000000000000..952a78e3408b --- /dev/null +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -0,0 +1,491 @@ +// Copyright 2019 The Chromium 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 'dart:async'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../utils/method_channel_mock.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelCamera', () { + group('Creation, Initialization & Disposal Tests', () { + test('Should send creation data and receive back a camera id', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1} + }); + final camera = MethodChannelCamera(); + + // Act + final cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + + // Assert + expect(cameraMockChannel.log, [ + isMethodCall( + 'create', + arguments: { + 'cameraName': 'Test', + 'resolutionPreset': 'high', + 'enableAudio': null + }, + ), + ]); + expect(cameraId, 1); + }); + + test( + 'Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final camera = MethodChannelCamera(); + + // Act + expect( + () => camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ), + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test( + 'Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final camera = MethodChannelCamera(); + + // Act + expect( + () => camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ), + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should send initialization data', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }); + final camera = MethodChannelCamera(); + final cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + + // Act + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController + .add(CameraInitializedEvent(cameraId, 1920, 1080)); + await initializeFuture; + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + isMethodCall( + 'initialize', + arguments: { + 'cameraId': 1, + }, + ), + ]); + }); + + test('Should send a disposal call on dispose', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null, + 'dispose': {'cameraId': 1} + }); + + final camera = MethodChannelCamera(); + final cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController + .add(CameraInitializedEvent(cameraId, 1920, 1080)); + await initializeFuture; + + // Act + await camera.dispose(cameraId); + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + anything, + isMethodCall( + 'dispose', + arguments: {'cameraId': 1}, + ), + ]); + }); + }); + + group('Event Tests', () { + MethodChannelCamera camera; + int cameraId; + setUp(() async { + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = MethodChannelCamera(); + cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController + .add(CameraInitializedEvent(cameraId, 1920, 1080)); + await initializeFuture; + }); + + test('Should receive initialized event', () async { + // Act + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = CameraInitializedEvent(cameraId, 3840, 2160); + await camera.handleMethodCall( + MethodCall('initialized', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive resolution changes', () async { + // Act + final Stream resolutionStream = + camera.onCameraResolutionChanged(cameraId); + final streamQueue = StreamQueue(resolutionStream); + + // Emit test events + final fhdEvent = CameraResolutionChangedEvent(cameraId, 1920, 1080); + final uhdEvent = CameraResolutionChangedEvent(cameraId, 3840, 2160); + await camera.handleMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + await camera.handleMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera closing events', () async { + // Act + final Stream eventStream = + camera.onCameraClosing(cameraId); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = CameraClosingEvent(cameraId); + await camera.handleMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera error events', () async { + // Act + final errorStream = camera.onCameraError(cameraId); + final streamQueue = StreamQueue(errorStream); + + // Emit test events + final event = CameraErrorEvent(cameraId, 'Error Description'); + await camera.handleMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleMethodCall( + MethodCall('error', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + }); + + group('Function Tests', () { + MethodChannelCamera camera; + int cameraId; + setUp(() async { + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = MethodChannelCamera(); + cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController + .add(CameraInitializedEvent(cameraId, 1920, 1080)); + await initializeFuture; + }); + + test('Should fetch CameraDescription instances for available cameras', + () async { + // Arrange + List> returnData = [ + {'name': 'Test 1', 'lensFacing': 'front', 'sensorOrientation': 1}, + {'name': 'Test 2', 'lensFacing': 'back', 'sensorOrientation': 2} + ]; + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'availableCameras': returnData}, + ); + + // Act + List cameras = await camera.availableCameras(); + + // Assert + expect(channel.log, [ + isMethodCall('availableCameras', arguments: null), + ]); + expect(cameras.length, returnData.length); + for (int i = 0; i < returnData.length; i++) { + CameraDescription cameraDescription = CameraDescription( + name: returnData[i]['name'], + lensDirection: + parseCameraLensDirection(returnData[i]['lensFacing']), + sensorOrientation: returnData[i]['sensorOrientation'], + ); + expect(cameras[i], cameraDescription); + } + }); + + test( + 'Should throw CameraException when availableCameras throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'availableCameras': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + + // Act + expect( + camera.availableCameras, + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should take a picture and return an XFile instance', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'takePicture': '/test/path.jpg'}); + + // Act + XFile file = await camera.takePicture(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('takePicture', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.jpg'); + }); + + test('Should prepare for video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'prepareForVideoRecording': null}, + ); + + // Act + await camera.prepareForVideoRecording(); + + // Assert + expect(channel.log, [ + isMethodCall('prepareForVideoRecording', arguments: null), + ]); + }); + + test('Should start recording a video', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should stop a video recording and return the file', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'stopVideoRecording': '/test/path.mp4'}, + ); + + // Act + XFile file = await camera.stopVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('stopVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.mp4'); + }); + + test('Should pause a video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'pauseVideoRecording': null}, + ); + + // Act + await camera.pauseVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pauseVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should resume a video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'resumeVideoRecording': null}, + ); + + // Act + await camera.resumeVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumeVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should build a texture widget as preview widget', () async { + // Act + Widget widget = camera.buildPreview(cameraId); + + // Act + expect(widget is Texture, isTrue); + expect((widget as Texture).textureId, cameraId); + }); + + test('Should throw MissingPluginException when handling unknown method', + () { + final camera = MethodChannelCamera(); + + expect(() => camera.handleMethodCall(MethodCall('unknown_method'), 1), + throwsA(isA())); + }); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/camera_description_test.dart b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart new file mode 100644 index 000000000000..03909dbafb97 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart @@ -0,0 +1,113 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraLensDirection tests', () { + test('CameraLensDirection should contain 3 options', () { + final values = CameraLensDirection.values; + + expect(values.length, 3); + }); + + test("CameraLensDirection enum should have items in correct index", () { + final values = CameraLensDirection.values; + + expect(values[0], CameraLensDirection.front); + expect(values[1], CameraLensDirection.back); + expect(values[2], CameraLensDirection.external); + }); + }); + + group('CameraDescription tests', () { + test('Constructor should initialize all properties', () { + final description = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(description.name, 'Test'); + expect(description.lensDirection, CameraLensDirection.front); + expect(description.sensorOrientation, 90); + }); + + test('equals should return true if objects are the same', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, true); + }); + + test('equals should return false if name is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Testing', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, false); + }); + + test('equals should return false if lens direction is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, false); + }); + + test('equals should return true if sensor orientation is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, true); + }); + + test('hashCode should match hashCode of all properties', () { + final description = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + final expectedHashCode = description.name.hashCode ^ + description.lensDirection.hashCode ^ + description.sensorOrientation.hashCode; + + expect(description.hashCode, expectedHashCode); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart b/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart new file mode 100644 index 000000000000..17370e4561f5 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart @@ -0,0 +1,28 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('constructor should initialize properties', () { + final code = 'TEST_ERROR'; + final description = 'This is a test error'; + final exception = CameraException(code, description); + + expect(exception.code, code); + expect(exception.description, description); + }); + + test('toString: Should return a description of the exception', () { + final code = 'TEST_ERROR'; + final description = 'This is a test error'; + final expected = 'CameraException($code, $description)'; + final exception = CameraException(code, description); + + final actual = exception.toString(); + + expect(actual, expected); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart b/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart new file mode 100644 index 000000000000..aadf589f87c4 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart @@ -0,0 +1,25 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('ResolutionPreset should contain 6 options', () { + final values = ResolutionPreset.values; + + expect(values.length, 6); + }); + + test("ResolutionPreset enum should have items in correct index", () { + final values = ResolutionPreset.values; + + expect(values[0], ResolutionPreset.low); + expect(values[1], ResolutionPreset.medium); + expect(values[2], ResolutionPreset.high); + expect(values[3], ResolutionPreset.veryHigh); + expect(values[4], ResolutionPreset.ultraHigh); + expect(values[5], ResolutionPreset.max); + }); +} diff --git a/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart new file mode 100644 index 000000000000..cdf393f82b5f --- /dev/null +++ b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart @@ -0,0 +1,38 @@ +// Copyright 2019 The Chromium 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/services.dart'; + +class MethodChannelMock { + final Duration delay; + final MethodChannel methodChannel; + final Map methods; + final log = []; + + MethodChannelMock({ + String channelName, + this.delay, + this.methods, + }) : methodChannel = MethodChannel(channelName) { + methodChannel.setMockMethodCallHandler(_handler); + } + + Future _handler(MethodCall methodCall) async { + log.add(methodCall); + + if (!methods.containsKey(methodCall.method)) { + throw MissingPluginException('No implementation found for method ' + '${methodCall.method} on channel ${methodChannel.name}'); + } + + return Future.delayed(delay ?? Duration.zero, () { + final result = methods[methodCall.method]; + if (result is Exception) { + throw result; + } + + return Future.value(result); + }); + } +} diff --git a/packages/camera/camera_platform_interface/test/utils/utils_test.dart b/packages/camera/camera_platform_interface/test/utils/utils_test.dart new file mode 100644 index 000000000000..dccb30754f14 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/utils/utils_test.dart @@ -0,0 +1,33 @@ +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Utility methods', () { + test( + 'Should return CameraLensDirection when valid value is supplied when parsing camera lens direction', + () { + expect( + parseCameraLensDirection('back'), + CameraLensDirection.back, + ); + expect( + parseCameraLensDirection('front'), + CameraLensDirection.front, + ); + expect( + parseCameraLensDirection('external'), + CameraLensDirection.external, + ); + }); + + test( + 'Should throw ArgumentException when invalid value is supplied when parsing camera lens direction', + () { + expect( + () => parseCameraLensDirection('test'), + throwsA(isArgumentError), + ); + }); + }); +}