From 2197235931baf8fda76fb226ddc1b4c7aa474a75 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Wed, 30 Dec 2020 16:53:13 +0100 Subject: [PATCH 01/25] Fixed video orientation on iOS --- .../camera/example/ios/Runner/Info.plist | 1 + packages/camera/camera/example/lib/main.dart | 18 +-- .../camera/camera/ios/Classes/CameraPlugin.m | 123 ++++++++++++------ .../camera/lib/src/camera_controller.dart | 4 +- .../camera/camera/lib/src/camera_preview.dart | 19 ++- 5 files changed, 112 insertions(+), 53 deletions(-) diff --git a/packages/camera/camera/example/ios/Runner/Info.plist b/packages/camera/camera/example/ios/Runner/Info.plist index f389a129e028..ff2e341a1803 100644 --- a/packages/camera/camera/example/ios/Runner/Info.plist +++ b/packages/camera/camera/example/ios/Runner/Info.plist @@ -39,6 +39,7 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index ee8e2c259b3d..b9c3b647cf71 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -138,14 +138,16 @@ class _CameraExampleHomeState extends State ); } else { return AspectRatio( - aspectRatio: controller.value.aspectRatio, - child: Listener( - onPointerDown: (_) => _pointers++, - onPointerUp: (_) => _pointers--, - child: GestureDetector( - onScaleStart: _handleScaleStart, - onScaleUpdate: _handleScaleUpdate, - child: CameraPreview(controller), + aspectRatio: 1 / controller.value.aspectRatio, + child: CameraPreview( + controller, + child: Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: GestureDetector( + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + ), ), ), ); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d54695233bdb..9e03367851ea 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -18,8 +18,8 @@ @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @property(readonly, nonatomic) FlutterResult result; -@property(readonly, nonatomic) CMMotionManager *motionManager; @property(readonly, nonatomic) AVCaptureDevicePosition cameraPosition; +@property(readonly, nonatomic) int deviceRotation; @end @interface FLTImageStreamHandler : NSObject @@ -47,13 +47,13 @@ @implementation FLTSavePhotoDelegate { - initWithPath:(NSString *)path result:(FlutterResult)result - motionManager:(CMMotionManager *)motionManager - cameraPosition:(AVCaptureDevicePosition)cameraPosition { + cameraPosition:(AVCaptureDevicePosition)cameraPosition + deviceRotation:(int)deviceRotation { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - _motionManager = motionManager; _cameraPosition = cameraPosition; + _deviceRotation = deviceRotation; selfReference = self; _result = result; return self; @@ -73,10 +73,11 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output NSData *data = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer]; + UIImage *image = [UIImage imageWithCGImage:[UIImage imageWithData:data].CGImage scale:1.0 - orientation:[self getImageRotation]]; - + orientation:[self getImageRotation:_cameraPosition + deviceRotation:_deviceRotation]]; // TODO(sigurdm): Consider writing file asynchronously. bool success = [UIImageJPEGRepresentation(image, 1.0) writeToFile:_path atomically:YES]; if (!success) { @@ -105,32 +106,22 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output _result(_path); } -- (UIImageOrientation)getImageRotation { - float const threshold = 45.0; - BOOL (^isNearValue)(float value1, float value2) = ^BOOL(float value1, float value2) { - return fabsf(value1 - value2) < threshold; - }; - BOOL (^isNearValueABS)(float value1, float value2) = ^BOOL(float value1, float value2) { - return isNearValue(fabsf(value1), fabsf(value2)); - }; - float yxAtan = (atan2(_motionManager.accelerometerData.acceleration.y, - _motionManager.accelerometerData.acceleration.x)) * - 180 / M_PI; - if (isNearValue(-90.0, yxAtan)) { - return UIImageOrientationRight; - } else if (isNearValueABS(180.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationUp - : UIImageOrientationDown; - } else if (isNearValueABS(0.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationDown /*rotate 180* */ - : UIImageOrientationUp /*do not rotate*/; - } else if (isNearValue(90.0, yxAtan)) { - return UIImageOrientationLeft; +- (UIImageOrientation)getImageRotation:(AVCaptureDevicePosition)_cameraPosition + deviceRotation:(int)deviceRotation { + switch (deviceRotation) { + case 90: + return UIImageOrientationRight; + case 180: + return _cameraPosition == AVCaptureDevicePositionBack + ? UIImageOrientationDown /*rotate 180* */ + : UIImageOrientationUp /*do not rotate*/; + case 270: + return UIImageOrientationLeft; + case 0: + default: + return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationUp + : UIImageOrientationDown; } - // If none of the above, then the device is likely facing straight down or straight up -- just - // pick something arbitrary - // TODO: Maybe use the UIInterfaceOrientation if in these scenarios - return UIImageOrientationUp; } @end @@ -297,7 +288,7 @@ - (instancetype)initWithCameraName:(NSString *)cameraName if ([_captureDevice position] == AVCaptureDevicePositionFront) { connection.videoMirrored = YES; } - connection.videoOrientation = AVCaptureVideoOrientationPortrait; + connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight; [_captureSession addInputWithNoConnections:_captureVideoInput]; [_captureSession addOutputWithNoConnections:_captureVideoOutput]; [_captureSession addConnection:connection]; @@ -327,6 +318,7 @@ - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { if (_resolutionPreset == max) { [settings setHighResolutionPhotoEnabled:YES]; } + AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(_flashMode); if (avFlashMode != -1) { [settings setFlashMode:avFlashMode]; @@ -340,12 +332,32 @@ - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { result(getFlutterError(error)); return; } - [_capturePhotoOutput - capturePhotoWithSettings:settings - delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path - result:result - motionManager:_motionManager - cameraPosition:_captureDevice.position]]; + + [_capturePhotoOutput capturePhotoWithSettings:settings + delegate:[[FLTSavePhotoDelegate alloc] + initWithPath:path + result:result + cameraPosition:_captureDevice.position + deviceRotation:[self getDeviceRotation]]]; +} + +- (AVCaptureVideoOrientation)getVideoOrientation:(AVCaptureDevicePosition)_cameraPosition + deviceRotation:(int)deviceRotation { + switch (deviceRotation) { + case 90: + return AVCaptureVideoOrientationLandscapeRight; + case 180: + return _cameraPosition == AVCaptureDevicePositionBack + ? AVCaptureVideoOrientationPortraitUpsideDown /*rotate 180* */ + : AVCaptureVideoOrientationPortrait /*do not rotate*/; + case 270: + return AVCaptureVideoOrientationLandscapeLeft; + case 0: + default: + return _cameraPosition == AVCaptureDevicePositionBack + ? AVCaptureVideoOrientationPortrait + : AVCaptureVideoOrientationPortraitUpsideDown; + } } - (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension @@ -862,7 +874,7 @@ - (BOOL)setupWriterForPath:(NSString *)path { [self setUpCaptureSessionForAudio]; } _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL - fileType:AVFileTypeQuickTimeMovie + fileType:AVFileTypeMPEG4 error:&error]; NSParameterAssert(_videoWriter); if (error) { @@ -871,8 +883,8 @@ - (BOOL)setupWriterForPath:(NSString *)path { } NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:AVVideoCodecH264, AVVideoCodecKey, - [NSNumber numberWithInt:_previewSize.height], AVVideoWidthKey, - [NSNumber numberWithInt:_previewSize.width], AVVideoHeightKey, + [NSNumber numberWithInt:_previewSize.width], AVVideoWidthKey, + [NSNumber numberWithInt:_previewSize.height], AVVideoHeightKey, nil]; _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings]; @@ -884,6 +896,8 @@ - (BOOL)setupWriterForPath:(NSString *)path { }]; NSParameterAssert(_videoWriterInput); + CGFloat rotationDegrees = [self getDeviceRotation]; + _videoWriterInput.transform = CGAffineTransformMakeRotation(rotationDegrees * M_PI / 180); _videoWriterInput.expectsMediaDataInRealTime = YES; // Add the audio input @@ -918,6 +932,7 @@ - (BOOL)setupWriterForPath:(NSString *)path { return YES; } + - (void)setUpCaptureSessionForAudio { NSError *error = nil; // Create a device input with the device and add it to the session. @@ -944,6 +959,32 @@ - (void)setUpCaptureSessionForAudio { } } } + +- (int)getDeviceRotation { + float const threshold = 45.0; + BOOL (^isNearValue)(float value1, float value2) = ^BOOL(float value1, float value2) { + return fabsf(value1 - value2) < threshold; + }; + BOOL (^isNearValueABS)(float value1, float value2) = ^BOOL(float value1, float value2) { + return isNearValue(fabsf(value1), fabsf(value2)); + }; + float yxAtan = (atan2(_motionManager.accelerometerData.acceleration.y, + _motionManager.accelerometerData.acceleration.x)) * + 180 / M_PI; + if (isNearValue(-90.0, yxAtan)) { + return 90; + } else if (isNearValueABS(180.0, yxAtan)) { + return 0; + } else if (isNearValueABS(0.0, yxAtan)) { + return 180; + } else if (isNearValue(90.0, yxAtan)) { + return 270; + } + // If none of the above, then the device is likely facing straight down or straight up -- just + // pick something arbitrary + return 0; +} + @end @interface CameraPlugin () diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 1d7aed755f42..c639e250391c 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -78,10 +78,10 @@ class CameraValue { /// Is `null` until [isInitialized] is `true`. final Size previewSize; - /// Convenience getter for `previewSize.height / previewSize.width`. + /// Convenience getter for `previewSize.width / previewSize.height`. /// /// Can only be called when [initialize] is done. - double get aspectRatio => previewSize.height / previewSize.width; + double get aspectRatio => previewSize.width / previewSize.height; /// Whether the controller is in an error state. /// diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index bf7862eb9151..19538fb0b771 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -4,20 +4,35 @@ import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// A widget showing a live camera preview. class CameraPreview extends StatelessWidget { /// Creates a preview widget for the given camera controller. - const CameraPreview(this.controller); + const CameraPreview(this.controller, {this.child}); /// The controller for the camera that the preview is shown for. final CameraController controller; + /// A widget to overlay on top of the camera preview + final Widget child; + @override Widget build(BuildContext context) { return controller.value.isInitialized - ? CameraPlatform.instance.buildPreview(controller.cameraId) + ? RotatedBox( + quarterTurns: _getQuarterTurns(MediaQuery.of(context).orientation), + child: AspectRatio( + aspectRatio: 1 / controller.value.aspectRatio, + child: CameraPlatform.instance.buildPreview(controller.cameraId), + ), + ) : Container(); } + + int _getQuarterTurns(Orientation orientation) { + int platformOffset = defaultTargetPlatform == TargetPlatform.iOS ? 1 : 0; + return platformOffset; + } } From 5988830f4e2e14c1a8f98f8fac0c12b6eec12a91 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 4 Jan 2021 10:32:34 +0100 Subject: [PATCH 02/25] Remove unnecessary check --- packages/camera/camera/lib/src/camera_preview.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 19538fb0b771..269600073244 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -22,7 +22,7 @@ class CameraPreview extends StatelessWidget { Widget build(BuildContext context) { return controller.value.isInitialized ? RotatedBox( - quarterTurns: _getQuarterTurns(MediaQuery.of(context).orientation), + quarterTurns: _getQuarterTurns(), child: AspectRatio( aspectRatio: 1 / controller.value.aspectRatio, child: CameraPlatform.instance.buildPreview(controller.cameraId), @@ -31,7 +31,7 @@ class CameraPreview extends StatelessWidget { : Container(); } - int _getQuarterTurns(Orientation orientation) { + int _getQuarterTurns() { int platformOffset = defaultTargetPlatform == TargetPlatform.iOS ? 1 : 0; return platformOffset; } From 9c4d4ca69a8a160b2ecc471a52f0b5c5732ce8d9 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 4 Jan 2021 13:02:30 +0100 Subject: [PATCH 03/25] Expand platform interface to support reporting device orientation --- .../lib/camera_platform_interface.dart | 1 + .../lib/src/events/device_event.dart | 45 ++++++++++++++ .../method_channel/method_channel_camera.dart | 58 +++++++++++++++--- .../platform_interface/camera_platform.dart | 7 +++ .../lib/src/types/device_orientation.dart | 50 ++++++++++++++++ .../lib/src/types/types.dart | 1 + .../test/camera_platform_interface_test.dart | 13 ++++ .../test/events/device_event_test.dart | 60 +++++++++++++++++++ .../method_channel_camera_test.dart | 52 ++++++++++++---- .../test/types/device_orientation_test.dart | 44 ++++++++++++++ 10 files changed, 309 insertions(+), 22 deletions(-) create mode 100644 packages/camera/camera_platform_interface/lib/src/events/device_event.dart create mode 100644 packages/camera/camera_platform_interface/lib/src/types/device_orientation.dart create mode 100644 packages/camera/camera_platform_interface/test/events/device_event_test.dart create mode 100644 packages/camera/camera_platform_interface/test/types/device_orientation_test.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 index eaa14da45a5e..d7e5fcd0834c 100644 --- a/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart +++ b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. export 'src/events/camera_event.dart'; +export 'src/events/device_event.dart'; export 'src/platform_interface/camera_platform.dart'; export 'src/types/types.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart new file mode 100644 index 000000000000..bfc9d9c6d518 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -0,0 +1,45 @@ +import 'package:camera_platform_interface/camera_platform_interface.dart'; + +/// Generic Event coming from the native side of Camera, that are not related to a specific camera module. +/// +/// This class is used as a base class for all the events that might be +/// triggered from a device, but it is never used directly as an event type. +/// +/// Do NOT instantiate new events like `DeviceEvent(cameraId)` directly, +/// use a specific class instead: +/// +/// Do `class NewEvent extend DeviceEvent` when creating your own events. +/// See below for examples: `DeviceOrientationChangedEvent`... +/// 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 DeviceEvent {} + +/// An event fired whenever. +class DeviceOrientationChangedEvent extends DeviceEvent { + /// The new orientation the device + final DeviceOrientation orientation; + + /// Build a new orientation changed event. + DeviceOrientationChangedEvent(this.orientation); + + /// Converts the supplied [Map] to an instance of the [DeviceOrientationChangedEvent] + /// class. + DeviceOrientationChangedEvent.fromJson(Map json) + : orientation = deserializeDeviceOrientation(json['orientation']); + + /// Converts the [DeviceOrientationChangedEvent] instance into a [Map] instance that + /// can be serialized to JSON. + Map toJson() => { + 'orientation': serializeDeviceOrientation(orientation), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DeviceOrientationChangedEvent && + runtimeType == other.runtimeType && + orientation == other.orientation; + + @override + int get hashCode => orientation.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 index 6a73031111df..fd6a053f558e 100644 --- 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 @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.dart'; import 'package:camera_platform_interface/src/utils/utils.dart'; import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; @@ -20,7 +21,7 @@ class MethodChannelCamera extends CameraPlatform { final Map _channels = {}; /// The controller we need to broadcast the different events coming - /// from handleMethodCall. + /// from handleMethodCall. Specific to camera events. /// /// It is a `broadcast` because multiple controllers will connect to /// different stream views of this Controller. @@ -30,10 +31,28 @@ class MethodChannelCamera extends CameraPlatform { final StreamController cameraEventStreamController = StreamController.broadcast(); - Stream _events(int cameraId) => + /// The controller we need to broadcast the different events coming + /// from handleMethodCall. Specific to general device events. + /// + /// 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 deviceEventStreamController = + StreamController.broadcast(); + + Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream .where((event) => event.cameraId == cameraId); + /// Construct a new method channel camera instance. + MethodChannelCamera() { + final channel = MethodChannel('flutter.io/cameraPlugin/device'); + channel.setMethodCallHandler( + (MethodCall call) => handleDeviceMethodCall(call)); + } + @override Future> availableCameras() async { try { @@ -80,7 +99,7 @@ class MethodChannelCamera extends CameraPlatform { _channels.putIfAbsent(cameraId, () { final channel = MethodChannel('flutter.io/cameraPlugin/camera$cameraId'); channel.setMethodCallHandler( - (MethodCall call) => handleMethodCall(call, cameraId)); + (MethodCall call) => handleCameraMethodCall(call, cameraId)); return channel; }); @@ -115,22 +134,28 @@ class MethodChannelCamera extends CameraPlatform { @override Stream onCameraInitialized(int cameraId) { - return _events(cameraId).whereType(); + return _cameraEvents(cameraId).whereType(); } @override Stream onCameraResolutionChanged(int cameraId) { - return _events(cameraId).whereType(); + return _cameraEvents(cameraId).whereType(); } @override Stream onCameraClosing(int cameraId) { - return _events(cameraId).whereType(); + return _cameraEvents(cameraId).whereType(); } @override Stream onCameraError(int cameraId) { - return _events(cameraId).whereType(); + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onDeviceOrientationChanged() { + return deviceEventStreamController.stream + .whereType(); } @override @@ -314,12 +339,27 @@ class MethodChannelCamera extends CameraPlatform { } } - /// Converts messages received from the native platform into events. + /// Converts messages received from the native platform into device 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. + Future handleDeviceMethodCall(MethodCall call) async { + switch (call.method) { + case 'orientation_changed': + deviceEventStreamController.add(DeviceOrientationChangedEvent( + deserializeDeviceOrientation(call.arguments['orientation']))); + break; + default: + throw MissingPluginException(); + } + } + + /// Converts messages received from the native platform into camera 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 { + Future handleCameraMethodCall(MethodCall call, int cameraId) async { switch (call.method) { case 'initialized': cameraEventStreamController.add(CameraInitializedEvent( 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 index c7a603228ce2..0d3fb55f0ccc 100644 --- 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 @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.dart'; import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; import 'package:camera_platform_interface/src/types/exposure_mode.dart'; import 'package:cross_file/cross_file.dart'; @@ -78,6 +79,12 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('onCameraError() is not implemented.'); } + /// The device orientation changed. + Stream onDeviceOrientationChanged() { + throw UnimplementedError( + 'onDeviceOrientationChanged() is not implemented.'); + } + /// Captures an image and returns the file where it was saved. Future takePicture(int cameraId) { throw UnimplementedError('takePicture() is not implemented.'); diff --git a/packages/camera/camera_platform_interface/lib/src/types/device_orientation.dart b/packages/camera/camera_platform_interface/lib/src/types/device_orientation.dart new file mode 100644 index 000000000000..78d2bb413e5a --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/device_orientation.dart @@ -0,0 +1,50 @@ +// 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. + +/// The possible orientations for the device. +enum DeviceOrientation { + /// Upright portrait orientation. + portrait, + + /// Upside down portrait orientation. + portraitUpsideDown, + + /// Landscape mode, with the bottom of the phone facing right. + landscapeRight, + + /// Landscape mode, with the bottom of the phone facing left. + landscapeLeft, +} + +/// Returns the device orientation as a String. +String serializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.portrait: + return 'portrait'; + case DeviceOrientation.portraitUpsideDown: + return 'portraitUpsideDown'; + case DeviceOrientation.landscapeRight: + return 'landscapeRight'; + case DeviceOrientation.landscapeLeft: + return 'landscapeLeft'; + default: + throw ArgumentError('Unknown DeviceOrientation value'); + } +} + +/// Returns the device orientation for a given String. +DeviceOrientation deserializeDeviceOrientation(String str) { + switch (str) { + case "portrait": + return DeviceOrientation.portrait; + case "portraitUpsideDown": + return DeviceOrientation.portraitUpsideDown; + case "landscapeRight": + return DeviceOrientation.landscapeRight; + case "landscapeLeft": + return DeviceOrientation.landscapeLeft; + default: + throw ArgumentError('"$str" is not a valid DeviceOrientation value'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index bab430eb5a69..93ede7be9d39 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -7,3 +7,4 @@ export 'resolution_preset.dart'; export 'camera_exception.dart'; export 'flash_mode.dart'; export 'exposure_mode.dart'; +export 'device_orientation.dart'; 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 index 574fa45e7b81..4695e6cd43c3 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -96,6 +96,19 @@ void main() { ); }); + test( + 'Default implementation of onDeviceOrientationChanged() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onDeviceOrientationChanged(), + throwsUnimplementedError, + ); + }); + test('Default implementation of dispose() should throw unimplemented error', () { // Arrange diff --git a/packages/camera/camera_platform_interface/test/events/device_event_test.dart b/packages/camera/camera_platform_interface/test/events/device_event_test.dart new file mode 100644 index 000000000000..c586b3525f99 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/events/device_event_test.dart @@ -0,0 +1,60 @@ +// 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('DeviceOrientationChangedEvent tests', () { + test('Constructor should initialize all properties', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portrait); + + expect(event.orientation, DeviceOrientation.portrait); + }); + + test('fromJson should initialize all properties', () { + final event = DeviceOrientationChangedEvent.fromJson({ + 'orientation': 'portrait', + }); + + expect(event.orientation, DeviceOrientation.portrait); + }); + + test('toJson should return a map with all fields', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portrait); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 1); + expect(jsonMap['orientation'], 'portrait'); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portrait); + final secondEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portrait); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if orientation is different', () { + final firstEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portrait); + final secondEvent = + DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portrait); + final expectedHashCode = event.orientation.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 index b916513ef0de..9784f51b2a23 100644 --- 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 @@ -7,9 +7,10 @@ import 'dart:math'; import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.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/services.dart' hide DeviceOrientation; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -223,7 +224,7 @@ void main() { ExposureMode.auto, true, ); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('initialized', event.toJson()), cameraId); // Assert @@ -242,13 +243,13 @@ void main() { // Emit test events final fhdEvent = CameraResolutionChangedEvent(cameraId, 1920, 1080); final uhdEvent = CameraResolutionChangedEvent(cameraId, 3840, 2160); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); // Assert @@ -269,11 +270,11 @@ void main() { // Emit test events final event = CameraClosingEvent(cameraId); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('camera_closing', event.toJson()), cameraId); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('camera_closing', event.toJson()), cameraId); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('camera_closing', event.toJson()), cameraId); // Assert @@ -292,11 +293,11 @@ void main() { // Emit test events final event = CameraErrorEvent(cameraId, 'Error Description'); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('error', event.toJson()), cameraId); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('error', event.toJson()), cameraId); - await camera.handleMethodCall( + await camera.handleCameraMethodCall( MethodCall('error', event.toJson()), cameraId); // Assert @@ -307,6 +308,29 @@ void main() { // Clean up await streamQueue.cancel(); }); + + test('Should receive device orientation change events', () async { + // Act + final eventStream = camera.onDeviceOrientationChanged(); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = DeviceOrientationChangedEvent(DeviceOrientation.portrait); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); }); group('Function Tests', () { @@ -685,7 +709,9 @@ void main() { () { final camera = MethodChannelCamera(); - expect(() => camera.handleMethodCall(MethodCall('unknown_method'), 1), + expect( + () => + camera.handleCameraMethodCall(MethodCall('unknown_method'), 1), throwsA(isA())); }); diff --git a/packages/camera/camera_platform_interface/test/types/device_orientation_test.dart b/packages/camera/camera_platform_interface/test/types/device_orientation_test.dart new file mode 100644 index 000000000000..e82a0e98b070 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/device_orientation_test.dart @@ -0,0 +1,44 @@ +// 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('DeviceOrientation should contain 4 options', () { + final values = DeviceOrientation.values; + + expect(values.length, 4); + }); + + test("DeviceOrientation enum should have items in correct index", () { + final values = DeviceOrientation.values; + + expect(values[0], DeviceOrientation.portrait); + expect(values[1], DeviceOrientation.portraitUpsideDown); + expect(values[2], DeviceOrientation.landscapeRight); + expect(values[3], DeviceOrientation.landscapeLeft); + }); + + test("serializeDeviceOrientation() should serialize correctly", () { + expect(serializeDeviceOrientation(DeviceOrientation.portrait), "portrait"); + expect(serializeDeviceOrientation(DeviceOrientation.portraitUpsideDown), + "portraitUpsideDown"); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeRight), + "landscapeRight"); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + "landscapeLeft"); + }); + + test("deserializeDeviceOrientation() should deserialize correctly", () { + expect( + deserializeDeviceOrientation('portrait'), DeviceOrientation.portrait); + expect(deserializeDeviceOrientation('portraitUpsideDown'), + DeviceOrientation.portraitUpsideDown); + expect(deserializeDeviceOrientation('landscapeRight'), + DeviceOrientation.landscapeRight); + expect(deserializeDeviceOrientation('landscapeLeft'), + DeviceOrientation.landscapeLeft); + }); +} From ed56faca7864a0fbf104d2859a010c8591eed432 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 4 Jan 2021 13:34:00 +0100 Subject: [PATCH 04/25] Switch to flutter DeviceOrientation enum --- .../lib/src/events/device_event.dart | 3 +- .../lib/src/types/device_orientation.dart | 50 ------------------- .../lib/src/types/types.dart | 1 - .../lib/src/utils/utils.dart | 33 ++++++++++++ .../test/events/device_event_test.dart | 21 ++++---- .../method_channel_camera_test.dart | 4 +- .../test/types/device_orientation_test.dart | 44 ---------------- .../test/utils/utils_test.dart | 23 +++++++++ 8 files changed, 72 insertions(+), 107 deletions(-) delete mode 100644 packages/camera/camera_platform_interface/lib/src/types/device_orientation.dart delete mode 100644 packages/camera/camera_platform_interface/test/types/device_orientation_test.dart diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart index bfc9d9c6d518..68728db02af6 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -1,4 +1,5 @@ -import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart'; /// Generic Event coming from the native side of Camera, that are not related to a specific camera module. /// diff --git a/packages/camera/camera_platform_interface/lib/src/types/device_orientation.dart b/packages/camera/camera_platform_interface/lib/src/types/device_orientation.dart deleted file mode 100644 index 78d2bb413e5a..000000000000 --- a/packages/camera/camera_platform_interface/lib/src/types/device_orientation.dart +++ /dev/null @@ -1,50 +0,0 @@ -// 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. - -/// The possible orientations for the device. -enum DeviceOrientation { - /// Upright portrait orientation. - portrait, - - /// Upside down portrait orientation. - portraitUpsideDown, - - /// Landscape mode, with the bottom of the phone facing right. - landscapeRight, - - /// Landscape mode, with the bottom of the phone facing left. - landscapeLeft, -} - -/// Returns the device orientation as a String. -String serializeDeviceOrientation(DeviceOrientation orientation) { - switch (orientation) { - case DeviceOrientation.portrait: - return 'portrait'; - case DeviceOrientation.portraitUpsideDown: - return 'portraitUpsideDown'; - case DeviceOrientation.landscapeRight: - return 'landscapeRight'; - case DeviceOrientation.landscapeLeft: - return 'landscapeLeft'; - default: - throw ArgumentError('Unknown DeviceOrientation value'); - } -} - -/// Returns the device orientation for a given String. -DeviceOrientation deserializeDeviceOrientation(String str) { - switch (str) { - case "portrait": - return DeviceOrientation.portrait; - case "portraitUpsideDown": - return DeviceOrientation.portraitUpsideDown; - case "landscapeRight": - return DeviceOrientation.landscapeRight; - case "landscapeLeft": - return DeviceOrientation.landscapeLeft; - default: - throw ArgumentError('"$str" is not a valid DeviceOrientation value'); - } -} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index 93ede7be9d39..bab430eb5a69 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -7,4 +7,3 @@ export 'resolution_preset.dart'; export 'camera_exception.dart'; export 'flash_mode.dart'; export 'exposure_mode.dart'; -export 'device_orientation.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 index f94d8e69c07e..5413f25bb8b7 100644 --- a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -1,4 +1,5 @@ import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; /// Parses a string into a corresponding CameraLensDirection. CameraLensDirection parseCameraLensDirection(String string) { @@ -12,3 +13,35 @@ CameraLensDirection parseCameraLensDirection(String string) { } throw ArgumentError('Unknown CameraLensDirection value'); } + +/// Returns the device orientation as a String. +String serializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.portraitUp: + return 'portraitUp'; + case DeviceOrientation.portraitDown: + return 'portraitDown'; + case DeviceOrientation.landscapeRight: + return 'landscapeRight'; + case DeviceOrientation.landscapeLeft: + return 'landscapeLeft'; + default: + throw ArgumentError('Unknown DeviceOrientation value'); + } +} + +/// Returns the device orientation for a given String. +DeviceOrientation deserializeDeviceOrientation(String str) { + switch (str) { + case "portraitUp": + return DeviceOrientation.portraitUp; + case "portraitDown": + return DeviceOrientation.portraitDown; + case "landscapeRight": + return DeviceOrientation.landscapeRight; + case "landscapeLeft": + return DeviceOrientation.landscapeLeft; + default: + throw ArgumentError('"$str" is not a valid DeviceOrientation value'); + } +} diff --git a/packages/camera/camera_platform_interface/test/events/device_event_test.dart b/packages/camera/camera_platform_interface/test/events/device_event_test.dart index c586b3525f99..c2fef49be04f 100644 --- a/packages/camera/camera_platform_interface/test/events/device_event_test.dart +++ b/packages/camera/camera_platform_interface/test/events/device_event_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -10,40 +11,40 @@ void main() { group('DeviceOrientationChangedEvent tests', () { test('Constructor should initialize all properties', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portrait); + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); - expect(event.orientation, DeviceOrientation.portrait); + expect(event.orientation, DeviceOrientation.portraitUp); }); test('fromJson should initialize all properties', () { final event = DeviceOrientationChangedEvent.fromJson({ - 'orientation': 'portrait', + 'orientation': 'portraitUp', }); - expect(event.orientation, DeviceOrientation.portrait); + expect(event.orientation, DeviceOrientation.portraitUp); }); test('toJson should return a map with all fields', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portrait); + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); final jsonMap = event.toJson(); expect(jsonMap.length, 1); - expect(jsonMap['orientation'], 'portrait'); + expect(jsonMap['orientation'], 'portraitUp'); }); test('equals should return true if objects are the same', () { final firstEvent = - DeviceOrientationChangedEvent(DeviceOrientation.portrait); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); final secondEvent = - DeviceOrientationChangedEvent(DeviceOrientation.portrait); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); expect(firstEvent == secondEvent, true); }); test('equals should return false if orientation is different', () { final firstEvent = - DeviceOrientationChangedEvent(DeviceOrientation.portrait); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); final secondEvent = DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft); @@ -51,7 +52,7 @@ void main() { }); test('hashCode should match hashCode of all properties', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portrait); + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); final expectedHashCode = event.orientation.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 index 9784f51b2a23..8c96d20c929a 100644 --- 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 @@ -11,6 +11,7 @@ import 'package:camera_platform_interface/src/events/device_event.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' hide DeviceOrientation; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -315,7 +316,8 @@ void main() { final streamQueue = StreamQueue(eventStream); // Emit test events - final event = DeviceOrientationChangedEvent(DeviceOrientation.portrait); + final event = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); await camera.handleDeviceMethodCall( MethodCall('orientation_changed', event.toJson())); await camera.handleDeviceMethodCall( diff --git a/packages/camera/camera_platform_interface/test/types/device_orientation_test.dart b/packages/camera/camera_platform_interface/test/types/device_orientation_test.dart deleted file mode 100644 index e82a0e98b070..000000000000 --- a/packages/camera/camera_platform_interface/test/types/device_orientation_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -// 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('DeviceOrientation should contain 4 options', () { - final values = DeviceOrientation.values; - - expect(values.length, 4); - }); - - test("DeviceOrientation enum should have items in correct index", () { - final values = DeviceOrientation.values; - - expect(values[0], DeviceOrientation.portrait); - expect(values[1], DeviceOrientation.portraitUpsideDown); - expect(values[2], DeviceOrientation.landscapeRight); - expect(values[3], DeviceOrientation.landscapeLeft); - }); - - test("serializeDeviceOrientation() should serialize correctly", () { - expect(serializeDeviceOrientation(DeviceOrientation.portrait), "portrait"); - expect(serializeDeviceOrientation(DeviceOrientation.portraitUpsideDown), - "portraitUpsideDown"); - expect(serializeDeviceOrientation(DeviceOrientation.landscapeRight), - "landscapeRight"); - expect(serializeDeviceOrientation(DeviceOrientation.landscapeLeft), - "landscapeLeft"); - }); - - test("deserializeDeviceOrientation() should deserialize correctly", () { - expect( - deserializeDeviceOrientation('portrait'), DeviceOrientation.portrait); - expect(deserializeDeviceOrientation('portraitUpsideDown'), - DeviceOrientation.portraitUpsideDown); - expect(deserializeDeviceOrientation('landscapeRight'), - DeviceOrientation.landscapeRight); - expect(deserializeDeviceOrientation('landscapeLeft'), - DeviceOrientation.landscapeLeft); - }); -} diff --git a/packages/camera/camera_platform_interface/test/utils/utils_test.dart b/packages/camera/camera_platform_interface/test/utils/utils_test.dart index dccb30754f14..63e3baff265d 100644 --- a/packages/camera/camera_platform_interface/test/utils/utils_test.dart +++ b/packages/camera/camera_platform_interface/test/utils/utils_test.dart @@ -1,5 +1,6 @@ import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -29,5 +30,27 @@ void main() { throwsA(isArgumentError), ); }); + + test("serializeDeviceOrientation() should serialize correctly", () { + expect(serializeDeviceOrientation(DeviceOrientation.portraitUp), + "portraitUp"); + expect(serializeDeviceOrientation(DeviceOrientation.portraitDown), + "portraitDown"); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeRight), + "landscapeRight"); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + "landscapeLeft"); + }); + + test("deserializeDeviceOrientation() should deserialize correctly", () { + expect(deserializeDeviceOrientation('portraitUp'), + DeviceOrientation.portraitUp); + expect(deserializeDeviceOrientation('portraitDown'), + DeviceOrientation.portraitDown); + expect(deserializeDeviceOrientation('landscapeRight'), + DeviceOrientation.landscapeRight); + expect(deserializeDeviceOrientation('landscapeLeft'), + DeviceOrientation.landscapeLeft); + }); }); } From 07931a78bf2986d5e850522925a57886af6b9f1d Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 4 Jan 2021 14:17:58 +0100 Subject: [PATCH 05/25] Fix preview rotation on iOS --- packages/camera/camera/example/lib/main.dart | 12 ++-- .../camera/camera/ios/Classes/CameraPlugin.m | 42 +++++++++++++ .../camera/lib/src/camera_controller.dart | 62 ++++++++++--------- .../camera/camera/lib/src/camera_preview.dart | 34 +++++++--- packages/camera/camera/pubspec.yaml | 4 +- 5 files changed, 109 insertions(+), 45 deletions(-) diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 5e909baa16c7..762569272924 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -157,17 +157,17 @@ class _CameraExampleHomeState extends State with WidgetsBindi ), ); } else { - return AspectRatio( - aspectRatio: 1 / controller.value.aspectRatio, - child: Listener( - onPointerDown: (_) => _pointers++, - onPointerUp: (_) => _pointers--, + return Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: CameraPreview( + controller, child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return GestureDetector( + behavior: HitTestBehavior.opaque, onScaleStart: _handleScaleStart, onScaleUpdate: _handleScaleUpdate, onTapDown: (details) => onViewFinderTap(details, constraints), - child: CameraPreview(controller), ); }), ), diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d140ef4f7248..406468833c44 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1086,6 +1086,7 @@ @interface CameraPlugin () @property(readonly, nonatomic) NSObject *registry; @property(readonly, nonatomic) NSObject *messenger; @property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel; @end @implementation CameraPlugin { @@ -1106,9 +1107,50 @@ - (instancetype)initWithRegistry:(NSObject *)registry NSAssert(self, @"super init cannot be nil"); _registry = registry; _messenger = messenger; + [self initDeviceEventMethodChannel]; + [self startOrientationListener]; return self; } +- (void)initDeviceEventMethodChannel { + _deviceEventMethodChannel = + [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" + binaryMessenger:_messenger]; +} + +- (void)startOrientationListener { + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(orientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; +} + +- (void)orientationChanged:(NSNotification *)note { + UIDevice *device = note.object; + + switch (device.orientation) { + case UIDeviceOrientationPortrait: + [_deviceEventMethodChannel invokeMethod:@"orientation_changed" + arguments:@{@"orientation" : @"portraitUp"}]; + break; + case UIDeviceOrientationPortraitUpsideDown: + [_deviceEventMethodChannel invokeMethod:@"orientation_changed" + arguments:@{@"orientation" : @"portraitDown"}]; + break; + case UIDeviceOrientationLandscapeRight: + [_deviceEventMethodChannel invokeMethod:@"orientation_changed" + arguments:@{@"orientation" : @"landscapeLeft"}]; + break; + case UIDeviceOrientationLandscapeLeft: + [_deviceEventMethodChannel invokeMethod:@"orientation_changed" + arguments:@{@"orientation" : @"landscapeRight"}]; + break; + default: + break; + }; +} + - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if (_dispatchQueue == nil) { _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 7b30b6056495..9fdf4f2015ba 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -41,6 +41,7 @@ class CameraValue { this.flashMode, this.exposureMode, this.exposurePointSupported, + this.deviceOrientation, }) : _isRecordingPaused = isRecordingPaused; /// Creates a new camera controller state for an uninitialized controller. @@ -53,6 +54,7 @@ class CameraValue { isRecordingPaused: false, flashMode: FlashMode.auto, exposurePointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, ); /// True after [CameraController.initialize] has completed successfully. @@ -102,6 +104,9 @@ class CameraValue { /// Whether setting the exposure point is supported. final bool exposurePointSupported; + /// The current device orientation. + final DeviceOrientation deviceOrientation; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -117,6 +122,7 @@ class CameraValue { FlashMode flashMode, ExposureMode exposureMode, bool exposurePointSupported, + DeviceOrientation deviceOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -128,8 +134,8 @@ class CameraValue { isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, flashMode: flashMode ?? this.flashMode, exposureMode: exposureMode ?? this.exposureMode, - exposurePointSupported: - exposurePointSupported ?? this.exposurePointSupported, + exposurePointSupported: exposurePointSupported ?? this.exposurePointSupported, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, ); } @@ -143,7 +149,8 @@ class CameraValue { 'isStreamingImages: $isStreamingImages, ' 'flashMode: $flashMode, ' 'exposureMode: $exposureMode, ' - 'exposurePointSupported: $exposurePointSupported)'; + 'exposurePointSupported: $exposurePointSupported, ' + 'deviceOrientation: $deviceOrientation)'; } } @@ -180,6 +187,7 @@ class CameraController extends ValueNotifier { bool _isDisposed = false; StreamSubscription _imageStreamSubscription; FutureOr _initCalled; + StreamSubscription _deviceOrientationSubscription; /// Checks whether [CameraController.dispose] has completed successfully. /// @@ -204,16 +212,19 @@ class CameraController extends ValueNotifier { try { Completer _initializeCompleter = Completer(); + _deviceOrientationSubscription = CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { + value = value.copyWith( + deviceOrientation: event.orientation, + ); + }); + _cameraId = await CameraPlatform.instance.createCamera( description, resolutionPreset, enableAudio: enableAudio, ); - unawaited(CameraPlatform.instance - .onCameraInitialized(_cameraId) - .first - .then((event) { + unawaited(CameraPlatform.instance.onCameraInitialized(_cameraId).first.then((event) { _initializeCompleter.complete(event); })); @@ -221,15 +232,12 @@ class CameraController extends ValueNotifier { value = value.copyWith( isInitialized: true, - previewSize: await _initializeCompleter.future - .then((CameraInitializedEvent event) => Size( - event.previewWidth, - event.previewHeight, - )), - exposureMode: await _initializeCompleter.future - .then((event) => event.exposureMode), - exposurePointSupported: await _initializeCompleter.future - .then((event) => event.exposurePointSupported), + previewSize: await _initializeCompleter.future.then((CameraInitializedEvent event) => Size( + event.previewWidth, + event.previewHeight, + )), + exposureMode: await _initializeCompleter.future.then((event) => event.exposureMode), + exposurePointSupported: await _initializeCompleter.future.then((event) => event.exposurePointSupported), ); } on PlatformException catch (e) { throw CameraException(e.code, e.message); @@ -298,8 +306,7 @@ class CameraController extends ValueNotifier { /// // TODO(bmparr): Add settings for resolution and fps. Future startImageStream(onLatestImageAvailable onAvailable) async { - assert(defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS); + assert(defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS); if (!value.isInitialized || _isDisposed) { throw CameraException( @@ -326,10 +333,8 @@ class CameraController extends ValueNotifier { } on PlatformException catch (e) { throw CameraException(e.code, e.message); } - const EventChannel cameraEventChannel = - EventChannel('plugins.flutter.io/camera/imageStream'); - _imageStreamSubscription = - cameraEventChannel.receiveBroadcastStream().listen( + const EventChannel cameraEventChannel = EventChannel('plugins.flutter.io/camera/imageStream'); + _imageStreamSubscription = cameraEventChannel.receiveBroadcastStream().listen( (dynamic imageData) { onAvailable(CameraImage.fromPlatformData(imageData)); }, @@ -344,8 +349,7 @@ class CameraController extends ValueNotifier { /// The `stopImageStream` method is only available on Android and iOS (other /// platforms won't be supported in current setup). Future stopImageStream() async { - assert(defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS); + assert(defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS); if (!value.isInitialized || _isDisposed) { throw CameraException( @@ -571,10 +575,8 @@ class CameraController extends ValueNotifier { /// Sets the exposure point for automatically determining the exposure value. Future setExposurePoint(Offset point) async { - if (point != null && - (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { - throw ArgumentError( - 'The values of point should be anywhere between (0,0) and (1,1).'); + if (point != null && (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError('The values of point should be anywhere between (0,0) and (1,1).'); } try { await CameraPlatform.instance.setExposurePoint( @@ -661,8 +663,7 @@ class CameraController extends ValueNotifier { } // Check if offset is in range - List range = - await Future.wait([getMinExposureOffset(), getMaxExposureOffset()]); + List range = await Future.wait([getMinExposureOffset(), getMaxExposureOffset()]); if (offset < range[0] || offset > range[1]) { throw CameraException( "exposureOffsetOutOfBounds", @@ -696,6 +697,7 @@ class CameraController extends ValueNotifier { if (_isDisposed) { return; } + unawaited(_deviceOrientationSubscription?.cancel()); _isDisposed = true; super.dispose(); if (_initCalled != null) { diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 269600073244..6d9f0ac67551 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -6,6 +6,7 @@ import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// A widget showing a live camera preview. class CameraPreview extends StatelessWidget { @@ -21,18 +22,35 @@ class CameraPreview extends StatelessWidget { @override Widget build(BuildContext context) { return controller.value.isInitialized - ? RotatedBox( - quarterTurns: _getQuarterTurns(), - child: AspectRatio( - aspectRatio: 1 / controller.value.aspectRatio, - child: CameraPlatform.instance.buildPreview(controller.cameraId), - ), - ) + ? AspectRatio( + aspectRatio: _isLandscape() ? controller.value.aspectRatio : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + RotatedBox( + quarterTurns: _getQuarterTurns(), + child: CameraPlatform.instance.buildPreview(controller.cameraId), + ), + child ?? Container(), + ], + ), + ) : Container(); } + bool _isLandscape() { + return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] + .contains(controller.value.deviceOrientation); + } + int _getQuarterTurns() { int platformOffset = defaultTargetPlatform == TargetPlatform.iOS ? 1 : 0; - return platformOffset; + Map turns = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.portraitDown: 2, + DeviceOrientation.landscapeRight: 3, + DeviceOrientation.landscapeLeft: 1 + }; + return turns[controller.value.deviceOrientation] + platformOffset; } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 2b392f17bf82..834bd8e1276c 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -8,7 +8,9 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter - camera_platform_interface: ^1.2.0 + # TODO (BeMacized): Change back to version reference when the platform interface updates have been published. + camera_platform_interface: + path: ../camera_platform_interface pedantic: ^1.8.0 dev_dependencies: From abe43603995c0522c2c8c79d2fbe0fe59453c803 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 4 Jan 2021 15:45:44 +0100 Subject: [PATCH 06/25] Fix preview rotation for android --- .../io/flutter/plugins/camera/Camera.java | 7 +- .../flutter/plugins/camera/CameraUtils.java | 35 ++++++ .../flutter/plugins/camera/DartMessenger.java | 66 ++++++++--- .../camera/DeviceOrientationListener.java | 72 ++++++++++++ .../plugins/camera/CameraUtilsTest.java | 22 ++++ .../plugins/camera/DartMessengerTest.java | 14 ++- packages/camera/camera/example/lib/main.dart | 110 +++++++++++++----- .../camera/camera/ios/Classes/CameraPlugin.m | 4 +- .../camera/lib/src/camera_controller.dart | 47 +++++--- .../camera/camera/lib/src/camera_preview.dart | 31 ++--- 10 files changed, 333 insertions(+), 75 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index a3ae3f275213..87ed8162220b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -63,6 +63,7 @@ public class Camera { private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; private final OrientationEventListener orientationEventListener; + private final DeviceOrientationListener deviceOrientationListener; private final boolean isFrontFacing; private final int sensorOrientation; private final String cameraName; @@ -117,10 +118,13 @@ public void onOrientationChanged(int i) { return; } // Convert the raw deg angle to the nearest multiple of 90. - currentOrientation = (int) Math.round(i / 90.0) * 90; + currentOrientation = (int) (Math.round(i / 90.0) * 90) % 360; } }; orientationEventListener.enable(); + deviceOrientationListener = + new DeviceOrientationListener(activity.getApplicationContext(), dartMessenger); + deviceOrientationListener.start(); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); @@ -976,6 +980,7 @@ public void dispose() { close(); flutterTexture.release(); orientationEventListener.disable(); + deviceOrientationListener.stop(); } private int getMediaOrientation() { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 3b665d6b24f2..892c9b3cbdf9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -10,6 +10,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; import java.util.Arrays; @@ -24,6 +25,40 @@ public final class CameraUtils { private CameraUtils() {} + static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { + // Round to the nearest 90 degrees. + degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; + // Determine the corresponding device orientation. + switch (degrees) { + case 90: + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case 180: + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case 270: + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + case 0: + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { + assert (orientation != null); + switch (orientation) { + case PORTRAIT_UP: + return "portraitUp"; + case PORTRAIT_DOWN: + return "portraitDown"; + case LANDSCAPE_LEFT: + return "landscapeLeft"; + case LANDSCAPE_RIGHT: + return "landscapeRight"; + default: + throw new UnsupportedOperationException( + "Could not serialize device orientation: " + orientation.toString()); + } + } + static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 2fee13816b51..1f7e43fa942f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -2,6 +2,7 @@ import android.text.TextUtils; import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.types.ExposureMode; @@ -9,16 +10,44 @@ import java.util.Map; class DartMessenger { - @Nullable private MethodChannel channel; + @Nullable private MethodChannel cameraChannel; + @Nullable private MethodChannel deviceChannel; - enum EventType { - ERROR, - CAMERA_CLOSING, - INITIALIZED, + enum DeviceEventType { + ORIENTATION_CHANGED("orientation_changed"); + private final String method; + + DeviceEventType(String method) { + this.method = method; + } + } + + enum CameraEventType { + ERROR("error"), + CLOSING("camera_closing"), + INITIALIZED("initialized"); + + private final String method; + + CameraEventType(String method) { + this.method = method; + } } DartMessenger(BinaryMessenger messenger, long cameraId) { - channel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); + cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); + deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); + } + + void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + assert (orientation != null); + this.send( + DeviceEventType.ORIENTATION_CHANGED, + new HashMap() { + { + put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); + } + }); } void sendCameraInitializedEvent( @@ -31,7 +60,7 @@ void sendCameraInitializedEvent( assert (exposureMode != null); assert (exposurePointSupported != null); this.send( - EventType.INITIALIZED, + CameraEventType.INITIALIZED, new HashMap() { { put("previewWidth", previewWidth.doubleValue()); @@ -43,12 +72,12 @@ void sendCameraInitializedEvent( } void sendCameraClosingEvent() { - send(EventType.CAMERA_CLOSING); + send(CameraEventType.CLOSING); } void sendCameraErrorEvent(@Nullable String description) { this.send( - EventType.ERROR, + CameraEventType.ERROR, new HashMap() { { if (!TextUtils.isEmpty(description)) put("description", description); @@ -56,14 +85,25 @@ void sendCameraErrorEvent(@Nullable String description) { }); } - void send(EventType eventType) { + void send(CameraEventType eventType) { + send(eventType, new HashMap<>()); + } + + void send(CameraEventType eventType, Map args) { + if (cameraChannel == null) { + return; + } + cameraChannel.invokeMethod(eventType.method, args); + } + + void send(DeviceEventType eventType) { send(eventType, new HashMap<>()); } - void send(EventType eventType, Map args) { - if (channel == null) { + void send(DeviceEventType eventType, Map args) { + if (deviceChannel == null) { return; } - channel.invokeMethod(eventType.toString().toLowerCase(), args); + deviceChannel.invokeMethod(eventType.method, args); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java new file mode 100644 index 000000000000..b287e56e36a6 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java @@ -0,0 +1,72 @@ +package io.flutter.plugins.camera; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; + +class DeviceOrientationListener { + private static final IntentFilter orientationIntentFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Context context; + private BroadcastReceiver broadcastReceiver; + private DartMessenger messenger; + private PlatformChannel.DeviceOrientation lastOrientation; + + public DeviceOrientationListener(Context context, DartMessenger messenger) { + this.context = context; + this.messenger = messenger; + } + + public void start() { + if (broadcastReceiver != null) return; + + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + PlatformChannel.DeviceOrientation orientation = getOrientation(); + if (orientation == null || lastOrientation == orientation) return; + lastOrientation = orientation; + messenger.sendDeviceOrientationChangeEvent(orientation); + } + }; + + context.registerReceiver(broadcastReceiver, orientationIntentFilter); + } + + public void stop() { + if (broadcastReceiver == null) return; + context.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + private PlatformChannel.DeviceOrientation getOrientation() { + final int rotation = + ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRotation(); + final int orientation = context.getResources().getConfiguration().orientation; + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return null; + } + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java new file mode 100644 index 000000000000..108baddbb744 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -0,0 +1,22 @@ +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; + +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import org.junit.Test; + +public class CameraUtilsTest { + + @Test + public void getDeviceOrientationFromDegrees_converts_correctly() { + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(0)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(-45)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(44)); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index f91bf82c7063..8e29e0cb3489 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; @@ -58,7 +59,7 @@ public void sendCameraErrorEvent_includesErrorDescriptions() { } @Test - public void sendCameraInitializedEvent_includesPreviewSize() { + public void sendCameraInitializedEvent_includesParams() { dartMessenger.sendCameraInitializedEvent(0, 0, ExposureMode.auto, true); List sentMessages = fakeBinaryMessenger.getMessages(); @@ -82,6 +83,17 @@ public void sendCameraClosingEvent() { assertNull(call.argument("description")); } + @Test + public void sendDeviceOrientationChangedEvent() { + dartMessenger.sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + + List sentMessages = fakeBinaryMessenger.getMessages(); + assertEquals(1, sentMessages.size()); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("orientation_changed", call.method); + assertEquals(call.argument("orientation"), "portraitUp"); + } + private MethodCall decodeSentMessage(ByteBuffer sentMessage) { sentMessage.position(0); diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 762569272924..9bb0321b7746 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -31,9 +31,11 @@ IconData getCameraLensIcon(CameraLensDirection direction) { throw ArgumentError('Unknown lens direction'); } -void logError(String code, String message) => print('Error: $code\nError Message: $message'); +void logError(String code, String message) => + print('Error: $code\nError Message: $message'); -class _CameraExampleHomeState extends State with WidgetsBindingObserver, TickerProviderStateMixin { +class _CameraExampleHomeState extends State + with WidgetsBindingObserver, TickerProviderStateMixin { CameraController controller; XFile imageFile; XFile videoFile; @@ -122,7 +124,9 @@ class _CameraExampleHomeState extends State with WidgetsBindi decoration: BoxDecoration( color: Colors.black, border: Border.all( - color: controller != null && controller.value.isRecordingVideo ? Colors.redAccent : Colors.grey, + color: controller != null && controller.value.isRecordingVideo + ? Colors.redAccent + : Colors.grey, width: 3.0, ), ), @@ -162,7 +166,8 @@ class _CameraExampleHomeState extends State with WidgetsBindi onPointerUp: (_) => _pointers--, child: CameraPreview( controller, - child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { return GestureDetector( behavior: HitTestBehavior.opaque, onScaleStart: _handleScaleStart, @@ -185,7 +190,8 @@ class _CameraExampleHomeState extends State with WidgetsBindi return; } - _currentScale = (_baseScale * details.scale).clamp(_minAvailableZoom, _maxAvailableZoom); + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); await controller.setZoomLevel(_currentScale); } @@ -207,10 +213,13 @@ class _CameraExampleHomeState extends State with WidgetsBindi child: Center( child: AspectRatio( aspectRatio: - videoController.value.size != null ? videoController.value.aspectRatio : 1.0, + videoController.value.size != null + ? videoController.value.aspectRatio + : 1.0, child: VideoPlayer(videoController)), ), - decoration: BoxDecoration(border: Border.all(color: Colors.pink)), + decoration: BoxDecoration( + border: Border.all(color: Colors.pink)), ), width: 64.0, height: 64.0, @@ -237,7 +246,8 @@ class _CameraExampleHomeState extends State with WidgetsBindi IconButton( icon: Icon(Icons.exposure), color: Colors.blue, - onPressed: controller != null ? onExposureModeButtonPressed : null, + onPressed: + controller != null ? onExposureModeButtonPressed : null, ), IconButton( icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), @@ -262,23 +272,39 @@ class _CameraExampleHomeState extends State with WidgetsBindi children: [ IconButton( icon: Icon(Icons.flash_off), - color: controller?.value?.flashMode == FlashMode.off ? Colors.orange : Colors.blue, - onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.off) : null, + color: controller?.value?.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.off) + : null, ), IconButton( icon: Icon(Icons.flash_auto), - color: controller?.value?.flashMode == FlashMode.auto ? Colors.orange : Colors.blue, - onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.auto) : null, + color: controller?.value?.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.auto) + : null, ), IconButton( icon: Icon(Icons.flash_on), - color: controller?.value?.flashMode == FlashMode.always ? Colors.orange : Colors.blue, - onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.always) : null, + color: controller?.value?.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.always) + : null, ), IconButton( icon: Icon(Icons.highlight), - color: controller?.value?.flashMode == FlashMode.torch ? Colors.orange : Colors.blue, - onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.torch) : null, + color: controller?.value?.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.torch) + : null, ), ], ), @@ -303,8 +329,14 @@ class _CameraExampleHomeState extends State with WidgetsBindi children: [ FlatButton( child: Text('AUTO'), - textColor: controller?.value?.exposureMode == ExposureMode.auto ? Colors.orange : Colors.blue, - onPressed: controller != null ? () => onSetExposureModeButtonPressed(ExposureMode.auto) : null, + textColor: + controller?.value?.exposureMode == ExposureMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.auto) + : null, onLongPress: () { if (controller != null) controller.setExposurePoint(null); showInSnackBar('Resetting exposure point'); @@ -312,8 +344,14 @@ class _CameraExampleHomeState extends State with WidgetsBindi ), FlatButton( child: Text('LOCKED'), - textColor: controller?.value?.exposureMode == ExposureMode.locked ? Colors.orange : Colors.blue, - onPressed: controller != null ? () => onSetExposureModeButtonPressed(ExposureMode.locked) : null, + textColor: + controller?.value?.exposureMode == ExposureMode.locked + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.locked) + : null, ), ], ), @@ -330,7 +368,10 @@ class _CameraExampleHomeState extends State with WidgetsBindi min: _minAvailableExposureOffset, max: _maxAvailableExposureOffset, label: _currentExposureOffset.toString(), - onChanged: _minAvailableExposureOffset == _maxAvailableExposureOffset ? null : setExposureOffset, + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, ), Text(_maxAvailableExposureOffset.toString()), ], @@ -351,21 +392,29 @@ class _CameraExampleHomeState extends State with WidgetsBindi IconButton( icon: const Icon(Icons.camera_alt), color: Colors.blue, - onPressed: controller != null && controller.value.isInitialized && !controller.value.isRecordingVideo + onPressed: controller != null && + controller.value.isInitialized && + !controller.value.isRecordingVideo ? onTakePictureButtonPressed : null, ), IconButton( icon: const Icon(Icons.videocam), color: Colors.blue, - onPressed: controller != null && controller.value.isInitialized && !controller.value.isRecordingVideo + onPressed: controller != null && + controller.value.isInitialized && + !controller.value.isRecordingVideo ? onVideoRecordButtonPressed : null, ), IconButton( - icon: controller != null && controller.value.isRecordingPaused ? Icon(Icons.play_arrow) : Icon(Icons.pause), + icon: controller != null && controller.value.isRecordingPaused + ? Icon(Icons.play_arrow) + : Icon(Icons.pause), color: Colors.blue, - onPressed: controller != null && controller.value.isInitialized && controller.value.isRecordingVideo + onPressed: controller != null && + controller.value.isInitialized && + controller.value.isRecordingVideo ? (controller != null && controller.value.isRecordingPaused ? onResumeButtonPressed : onPauseButtonPressed) @@ -374,7 +423,9 @@ class _CameraExampleHomeState extends State with WidgetsBindi IconButton( icon: const Icon(Icons.stop), color: Colors.red, - onPressed: controller != null && controller.value.isInitialized && controller.value.isRecordingVideo + onPressed: controller != null && + controller.value.isInitialized && + controller.value.isRecordingVideo ? onStopButtonPressed : null, ) @@ -397,7 +448,9 @@ class _CameraExampleHomeState extends State with WidgetsBindi title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), groupValue: controller?.description, value: cameraDescription, - onChanged: controller != null && controller.value.isRecordingVideo ? null : onNewCameraSelected, + onChanged: controller != null && controller.value.isRecordingVideo + ? null + : onNewCameraSelected, ), ), ); @@ -626,7 +679,8 @@ class _CameraExampleHomeState extends State with WidgetsBindi } Future _startVideoPlayer() async { - final VideoPlayerController vController = VideoPlayerController.file(File(videoFile.path)); + final VideoPlayerController vController = + VideoPlayerController.file(File(videoFile.path)); videoPlayerListener = () { if (videoController != null && videoController.value.size != null) { // Refreshing the state to update video player with the correct ratio. diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 406468833c44..3ee696dd5794 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1140,11 +1140,11 @@ - (void)orientationChanged:(NSNotification *)note { break; case UIDeviceOrientationLandscapeRight: [_deviceEventMethodChannel invokeMethod:@"orientation_changed" - arguments:@{@"orientation" : @"landscapeLeft"}]; + arguments:@{@"orientation" : @"landscapeRight"}]; break; case UIDeviceOrientationLandscapeLeft: [_deviceEventMethodChannel invokeMethod:@"orientation_changed" - arguments:@{@"orientation" : @"landscapeRight"}]; + arguments:@{@"orientation" : @"landscapeLeft"}]; break; default: break; diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 9fdf4f2015ba..d7ba1fb5f06b 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -134,7 +134,8 @@ class CameraValue { isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, flashMode: flashMode ?? this.flashMode, exposureMode: exposureMode ?? this.exposureMode, - exposurePointSupported: exposurePointSupported ?? this.exposurePointSupported, + exposurePointSupported: + exposurePointSupported ?? this.exposurePointSupported, deviceOrientation: deviceOrientation ?? this.deviceOrientation, ); } @@ -212,7 +213,8 @@ class CameraController extends ValueNotifier { try { Completer _initializeCompleter = Completer(); - _deviceOrientationSubscription = CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { + _deviceOrientationSubscription = + CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { value = value.copyWith( deviceOrientation: event.orientation, ); @@ -224,7 +226,10 @@ class CameraController extends ValueNotifier { enableAudio: enableAudio, ); - unawaited(CameraPlatform.instance.onCameraInitialized(_cameraId).first.then((event) { + unawaited(CameraPlatform.instance + .onCameraInitialized(_cameraId) + .first + .then((event) { _initializeCompleter.complete(event); })); @@ -232,12 +237,15 @@ class CameraController extends ValueNotifier { value = value.copyWith( isInitialized: true, - previewSize: await _initializeCompleter.future.then((CameraInitializedEvent event) => Size( - event.previewWidth, - event.previewHeight, - )), - exposureMode: await _initializeCompleter.future.then((event) => event.exposureMode), - exposurePointSupported: await _initializeCompleter.future.then((event) => event.exposurePointSupported), + previewSize: await _initializeCompleter.future + .then((CameraInitializedEvent event) => Size( + event.previewWidth, + event.previewHeight, + )), + exposureMode: await _initializeCompleter.future + .then((event) => event.exposureMode), + exposurePointSupported: await _initializeCompleter.future + .then((event) => event.exposurePointSupported), ); } on PlatformException catch (e) { throw CameraException(e.code, e.message); @@ -306,7 +314,8 @@ class CameraController extends ValueNotifier { /// // TODO(bmparr): Add settings for resolution and fps. Future startImageStream(onLatestImageAvailable onAvailable) async { - assert(defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS); + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); if (!value.isInitialized || _isDisposed) { throw CameraException( @@ -333,8 +342,10 @@ class CameraController extends ValueNotifier { } on PlatformException catch (e) { throw CameraException(e.code, e.message); } - const EventChannel cameraEventChannel = EventChannel('plugins.flutter.io/camera/imageStream'); - _imageStreamSubscription = cameraEventChannel.receiveBroadcastStream().listen( + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera/imageStream'); + _imageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen( (dynamic imageData) { onAvailable(CameraImage.fromPlatformData(imageData)); }, @@ -349,7 +360,8 @@ class CameraController extends ValueNotifier { /// The `stopImageStream` method is only available on Android and iOS (other /// platforms won't be supported in current setup). Future stopImageStream() async { - assert(defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS); + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); if (!value.isInitialized || _isDisposed) { throw CameraException( @@ -575,8 +587,10 @@ class CameraController extends ValueNotifier { /// Sets the exposure point for automatically determining the exposure value. Future setExposurePoint(Offset point) async { - if (point != null && (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { - throw ArgumentError('The values of point should be anywhere between (0,0) and (1,1).'); + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); } try { await CameraPlatform.instance.setExposurePoint( @@ -663,7 +677,8 @@ class CameraController extends ValueNotifier { } // Check if offset is in range - List range = await Future.wait([getMinExposureOffset(), getMaxExposureOffset()]); + List range = + await Future.wait([getMinExposureOffset(), getMaxExposureOffset()]); if (offset < range[0] || offset > range[1]) { throw CameraException( "exposureOffsetOutOfBounds", diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 6d9f0ac67551..4ee931a66a9a 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -23,18 +23,21 @@ class CameraPreview extends StatelessWidget { Widget build(BuildContext context) { return controller.value.isInitialized ? AspectRatio( - aspectRatio: _isLandscape() ? controller.value.aspectRatio : (1 / controller.value.aspectRatio), - child: Stack( - fit: StackFit.expand, - children: [ - RotatedBox( - quarterTurns: _getQuarterTurns(), - child: CameraPlatform.instance.buildPreview(controller.cameraId), - ), - child ?? Container(), - ], - ), - ) + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + RotatedBox( + quarterTurns: _getQuarterTurns(), + child: + CameraPlatform.instance.buildPreview(controller.cameraId), + ), + child ?? Container(), + ], + ), + ) : Container(); } @@ -48,8 +51,8 @@ class CameraPreview extends StatelessWidget { Map turns = { DeviceOrientation.portraitUp: 0, DeviceOrientation.portraitDown: 2, - DeviceOrientation.landscapeRight: 3, - DeviceOrientation.landscapeLeft: 1 + DeviceOrientation.landscapeRight: 1, + DeviceOrientation.landscapeLeft: 3 }; return turns[controller.value.deviceOrientation] + platformOffset; } From 4dcbbf7e255f111cd6a33462349d27d45a9a0f4c Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 4 Jan 2021 16:18:10 +0100 Subject: [PATCH 07/25] Update unit tests --- .../plugins/camera/CameraUtilsTest.java | 52 ++++++++++++++++++- packages/camera/camera/test/camera_test.dart | 7 +++ .../camera/camera/test/camera_value_test.dart | 6 ++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java index 108baddbb744..6bdea0662915 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -7,16 +7,66 @@ public class CameraUtilsTest { + @Test + public void serializeDeviceOrientation_serializes_correctly() { + assertEquals( + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + assertEquals( + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + assertEquals( + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + assertEquals( + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + } + @Test public void getDeviceOrientationFromDegrees_converts_correctly() { + // Portrait UP assertEquals( PlatformChannel.DeviceOrientation.PORTRAIT_UP, CameraUtils.getDeviceOrientationFromDegrees(0)); assertEquals( PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(-45)); + CameraUtils.getDeviceOrientationFromDegrees(315)); assertEquals( PlatformChannel.DeviceOrientation.PORTRAIT_UP, CameraUtils.getDeviceOrientationFromDegrees(44)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(-45)); + // Portrait DOWN + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(180)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(135)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(224)); + // Landscape LEFT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(90)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(45)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(134)); + // Landscape RIGHT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(270)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(225)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(314)); } } diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 5a4a7fc8771b..3c6b9389f5a1 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -30,6 +30,9 @@ get mockInitializeCamera => 13; get mockOnCameraInitializedEvent => CameraInitializedEvent(13, 75, 75, ExposureMode.auto, true); +get mockOnDeviceOrientationChangedEvent => + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + get mockOnCameraClosingEvent => null; get mockOnCameraErrorEvent => CameraErrorEvent(13, 'closing'); @@ -1029,6 +1032,10 @@ class MockCameraPlatform extends Mock Stream onCameraError(int cameraId) => Stream.value(mockOnCameraErrorEvent); + @override + Stream onDeviceOrientationChanged() => + Stream.value(mockOnDeviceOrientationChangedEvent); + @override Future takePicture(int cameraId) => mockPlatformException ? throw PlatformException(code: 'foo', message: 'bar') diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index d9193e212ea9..1c976bdf30c0 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -75,7 +76,7 @@ void main() { var cameraValue = cv.copyWith(isInitialized: true, previewSize: Size(20, 10)); - expect(cameraValue.aspectRatio, 0.5); + expect(cameraValue.aspectRatio, 2.0); }); test('hasError is true after setting errorDescription', () { @@ -107,10 +108,11 @@ void main() { isStreamingImages: false, flashMode: FlashMode.auto, exposurePointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: null, exposurePointSupported: true)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: null, exposurePointSupported: true, deviceOrientation: DeviceOrientation.portraitUp)'); }); }); } From 69f752de1f557394e924ffa281e98b0f3927ed3b Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 4 Jan 2021 16:30:12 +0100 Subject: [PATCH 08/25] Fix rotation on initialise. --- .../flutter/plugins/camera/DeviceOrientationListener.java | 2 ++ packages/camera/camera/ios/Classes/CameraPlugin.m | 6 +++++- packages/camera/camera/test/camera_test.dart | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java index b287e56e36a6..e85205bcd2bc 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java @@ -38,6 +38,8 @@ public void onReceive(Context context, Intent intent) { }; context.registerReceiver(broadcastReceiver, orientationIntentFilter); + // Trigger initial value + broadcastReceiver.onReceive(context, null); } public void stop() { diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 3ee696dd5794..1e81d5d5ad27 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -1128,8 +1128,11 @@ - (void)startOrientationListener { - (void)orientationChanged:(NSNotification *)note { UIDevice *device = note.object; + [self sendDeviceOrientation:device.orientation]; +} - switch (device.orientation) { +- (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { + switch (orientation) { case UIDeviceOrientationPortrait: [_deviceEventMethodChannel invokeMethod:@"orientation_changed" arguments:@{@"orientation" : @"portraitUp"}]; @@ -1246,6 +1249,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re @"exposurePointSupported" : @([_camera.captureDevice isExposurePointOfInterestSupported]), }]; + [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; [_camera start]; result(nil); } else if ([@"takePicture" isEqualToString:call.method]) { diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 3c6b9389f5a1..f4d5eeca60e9 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -31,7 +31,7 @@ get mockOnCameraInitializedEvent => CameraInitializedEvent(13, 75, 75, ExposureMode.auto, true); get mockOnDeviceOrientationChangedEvent => - DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); get mockOnCameraClosingEvent => null; From fb4204929c226d8b4f40818dfe9f1827de36675d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Tue, 5 Jan 2021 09:26:58 +0100 Subject: [PATCH 09/25] Keep EXIF data and picture orientation --- .../camera/camera/ios/Classes/CameraPlugin.m | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 816792e2fc1d..ae8f3c211d91 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -70,15 +70,14 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output _result(getFlutterError(error)); return; } + NSData *data = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer]; - UIImage *image = [UIImage imageWithCGImage:[UIImage imageWithData:data].CGImage - scale:1.0 - orientation:[self getImageRotation]]; // TODO(sigurdm): Consider writing file asynchronously. - bool success = [UIImageJPEGRepresentation(image, 1.0) writeToFile:_path atomically:YES]; + bool success = [data writeToFile:_path atomically:YES]; + if (!success) { _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); return; @@ -104,34 +103,6 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output } _result(_path); } - -- (UIImageOrientation)getImageRotation { - float const threshold = 45.0; - BOOL (^isNearValue)(float value1, float value2) = ^BOOL(float value1, float value2) { - return fabsf(value1 - value2) < threshold; - }; - BOOL (^isNearValueABS)(float value1, float value2) = ^BOOL(float value1, float value2) { - return isNearValue(fabsf(value1), fabsf(value2)); - }; - float yxAtan = (atan2(_motionManager.accelerometerData.acceleration.y, - _motionManager.accelerometerData.acceleration.x)) * - 180 / M_PI; - if (isNearValue(-90.0, yxAtan)) { - return UIImageOrientationRight; - } else if (isNearValueABS(180.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationUp - : UIImageOrientationDown; - } else if (isNearValueABS(0.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationDown /*rotate 180* */ - : UIImageOrientationUp /*do not rotate*/; - } else if (isNearValue(90.0, yxAtan)) { - return UIImageOrientationLeft; - } - // If none of the above, then the device is likely facing straight down or straight up -- just - // pick something arbitrary - // TODO: Maybe use the UIInterfaceOrientation if in these scenarios - return UIImageOrientationUp; -} @end // Mirrors FlashMode in flash_mode.dart @@ -384,6 +355,12 @@ - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { return; } + AVCaptureConnection *connection = [_capturePhotoOutput connectionWithMediaType:AVMediaTypeVideo]; + + if (connection) { + connection.videoOrientation = [self getVideoOrientation]; + } + [_capturePhotoOutput capturePhotoWithSettings:settings delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path @@ -392,6 +369,26 @@ - (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { cameraPosition:_captureDevice.position]]; } +- (AVCaptureVideoOrientation)getVideoOrientation { + UIDeviceOrientation deviceOrientation = [[Ution]; + + if (deviceOrientation == UIDeviceOrientationPortrait) { + return AVCaptureVideoOrientationPortrait; + } else if (deviceOrientation == UIDeviceOrientationLandscapeLeft) { + // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape left the video orientation should be landscape right. + return AVCaptureVideoOrientationLandscapeRight; + } else if (deviceOrientation == UIDeviceOrientationLandscapeRight) { + // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape right the video orientation should be landscape left. + return AVCaptureVideoOrientationLandscapeLeft; + } else if (deviceOrientation == UIDeviceOrientationPortraitUpsideDown) { + return AVCaptureVideoOrientationPortraitUpsideDown; + } else { + return AVCaptureVideoOrientationPortrait; + } +} + - (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension subfolder:(NSString *)subfolder prefix:(NSString *)prefix @@ -1010,10 +1007,12 @@ - (BOOL)setupWriterForPath:(NSString *)path { } [_videoWriter addInput:_videoWriterInput]; + [_captureVideoOutput setSampleBufferDelegate:self queue:_dispatchQueue]; return YES; } + - (void)setUpCaptureSessionForAudio { NSError *error = nil; // Create a device input with the device and add it to the session. From eab58fe4308b0deb3b199787564b2b7aec13e394 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 10:29:42 +0100 Subject: [PATCH 10/25] Add interface methods for (un)locking the capture orientation. --- .../platform_interface/camera_platform.dart | 13 ++++++++++ .../test/camera_platform_interface_test.dart | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+) 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 index 0d3fb55f0ccc..7d99a7c0145b 100644 --- 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 @@ -10,6 +10,7 @@ import 'package:camera_platform_interface/src/events/device_event.dart'; import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; import 'package:camera_platform_interface/src/types/exposure_mode.dart'; import 'package:cross_file/cross_file.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -85,6 +86,18 @@ abstract class CameraPlatform extends PlatformInterface { 'onDeviceOrientationChanged() is not implemented.'); } + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation([DeviceOrientation orientation]) { + throw UnimplementedError('lockCaptureOrientation() is not implemented.'); + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation() { + throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); + } + /// Captures an image and returns the file where it was saved. Future takePicture(int cameraId) { throw UnimplementedError('takePicture() is not implemented.'); 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 index 4695e6cd43c3..dafac09222db 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -109,6 +109,32 @@ void main() { ); }); + test( + 'Default implementation of lockCaptureOrientation() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.lockCaptureOrientation(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of unlockCaptureOrientation() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.unlockCaptureOrientation(), + throwsUnimplementedError, + ); + }); + test('Default implementation of dispose() should throw unimplemented error', () { // Arrange From 27bba292d8495fa8412db41a5f842be71375a46b Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 10:38:46 +0100 Subject: [PATCH 11/25] Update capture orientation interfaces and add unit tests. --- .../method_channel/method_channel_camera.dart | 20 ++++++++++ .../platform_interface/camera_platform.dart | 5 ++- .../lib/src/utils/utils.dart | 2 + .../test/camera_platform_interface_test.dart | 4 +- .../method_channel_camera_test.dart | 38 +++++++++++++++++++ .../test/utils/utils_test.dart | 2 + 6 files changed, 67 insertions(+), 4 deletions(-) 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 index fd6a053f558e..d1a55bc9eb8a 100644 --- 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 @@ -158,6 +158,26 @@ class MethodChannelCamera extends CameraPlatform { .whereType(); } + @override + Future lockCaptureOrientation(int cameraId, + [DeviceOrientation orientation]) async { + await _channel.invokeMethod( + 'lockCaptureOrientation', + { + 'cameraId': cameraId, + 'orientation': serializeDeviceOrientation(orientation) + }, + ); + } + + @override + Future unlockCaptureOrientation(int cameraId) async { + await _channel.invokeMethod( + 'unlockCaptureOrientation', + {'cameraId': cameraId}, + ); + } + @override Future takePicture(int cameraId) async { String path = await _channel.invokeMethod( 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 index 7d99a7c0145b..6fbf5821ddf3 100644 --- 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 @@ -89,12 +89,13 @@ abstract class CameraPlatform extends PlatformInterface { /// Locks the capture orientation. /// /// If [orientation] is omitted, the current device orientation is used. - Future lockCaptureOrientation([DeviceOrientation orientation]) { + Future lockCaptureOrientation(int cameraId, + [DeviceOrientation orientation]) { throw UnimplementedError('lockCaptureOrientation() is not implemented.'); } /// Unlocks the capture orientation. - Future unlockCaptureOrientation() { + Future unlockCaptureOrientation(int cameraId) { throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); } diff --git a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart index 5413f25bb8b7..beaefd6146df 100644 --- a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -16,6 +16,7 @@ CameraLensDirection parseCameraLensDirection(String string) { /// Returns the device orientation as a String. String serializeDeviceOrientation(DeviceOrientation orientation) { + if (orientation == null) return null; switch (orientation) { case DeviceOrientation.portraitUp: return 'portraitUp'; @@ -32,6 +33,7 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { /// Returns the device orientation for a given String. DeviceOrientation deserializeDeviceOrientation(String str) { + if (str == null) return null; switch (str) { case "portraitUp": return DeviceOrientation.portraitUp; 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 index dafac09222db..94cfc190305a 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -117,7 +117,7 @@ void main() { // Act & Assert expect( - () => cameraPlatform.lockCaptureOrientation(), + () => cameraPlatform.lockCaptureOrientation(1), throwsUnimplementedError, ); }); @@ -130,7 +130,7 @@ void main() { // Act & Assert expect( - () => cameraPlatform.unlockCaptureOrientation(), + () => cameraPlatform.unlockCaptureOrientation(1), throwsUnimplementedError, ); }); 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 index 8c96d20c929a..bf9430fd6079 100644 --- 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 @@ -794,6 +794,44 @@ void main() { .having((e) => e.description, 'description', 'Illegal zoom error'))); }); + + test('Should lock the capture orientation', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'lockCaptureOrientation': null}, + ); + + // Act + await camera.lockCaptureOrientation(cameraId); + await camera.lockCaptureOrientation( + cameraId, DeviceOrientation.portraitUp); + + // Assert + expect(channel.log, [ + isMethodCall('lockCaptureOrientation', + arguments: {'cameraId': cameraId, 'orientation': null}), + isMethodCall('lockCaptureOrientation', + arguments: {'cameraId': cameraId, 'orientation': 'portraitUp'}), + ]); + }); + + test('Should unlock the capture orientation', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'unlockCaptureOrientation': null}, + ); + + // Act + await camera.unlockCaptureOrientation(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('unlockCaptureOrientation', + arguments: {'cameraId': cameraId}), + ]); + }); }); }); } diff --git a/packages/camera/camera_platform_interface/test/utils/utils_test.dart b/packages/camera/camera_platform_interface/test/utils/utils_test.dart index 63e3baff265d..dc9163877d57 100644 --- a/packages/camera/camera_platform_interface/test/utils/utils_test.dart +++ b/packages/camera/camera_platform_interface/test/utils/utils_test.dart @@ -32,6 +32,7 @@ void main() { }); test("serializeDeviceOrientation() should serialize correctly", () { + expect(serializeDeviceOrientation(null), null); expect(serializeDeviceOrientation(DeviceOrientation.portraitUp), "portraitUp"); expect(serializeDeviceOrientation(DeviceOrientation.portraitDown), @@ -43,6 +44,7 @@ void main() { }); test("deserializeDeviceOrientation() should deserialize correctly", () { + expect(deserializeDeviceOrientation(null), null); expect(deserializeDeviceOrientation('portraitUp'), DeviceOrientation.portraitUp); expect(deserializeDeviceOrientation('portraitDown'), From 859076fb76bd1a9820a6be38d1a0671de283ee10 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 11:13:04 +0100 Subject: [PATCH 12/25] Made device orientation mandatory for locking capture orientation in the platform interface. --- .../lib/src/method_channel/method_channel_camera.dart | 4 ++-- .../lib/src/platform_interface/camera_platform.dart | 6 ++---- .../camera_platform_interface/lib/src/utils/utils.dart | 2 -- .../test/camera_platform_interface_test.dart | 2 +- .../test/method_channel/method_channel_camera_test.dart | 3 --- .../camera_platform_interface/test/utils/utils_test.dart | 2 -- 6 files changed, 5 insertions(+), 14 deletions(-) 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 index d1a55bc9eb8a..86ea39ec748a 100644 --- 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 @@ -159,8 +159,8 @@ class MethodChannelCamera extends CameraPlatform { } @override - Future lockCaptureOrientation(int cameraId, - [DeviceOrientation orientation]) async { + Future lockCaptureOrientation( + int cameraId, DeviceOrientation orientation) async { await _channel.invokeMethod( 'lockCaptureOrientation', { 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 index 6fbf5821ddf3..a6a93b98e181 100644 --- 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 @@ -87,10 +87,8 @@ abstract class CameraPlatform extends PlatformInterface { } /// Locks the capture orientation. - /// - /// If [orientation] is omitted, the current device orientation is used. - Future lockCaptureOrientation(int cameraId, - [DeviceOrientation orientation]) { + Future lockCaptureOrientation( + int cameraId, DeviceOrientation orientation) { throw UnimplementedError('lockCaptureOrientation() is not implemented.'); } diff --git a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart index beaefd6146df..5413f25bb8b7 100644 --- a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -16,7 +16,6 @@ CameraLensDirection parseCameraLensDirection(String string) { /// Returns the device orientation as a String. String serializeDeviceOrientation(DeviceOrientation orientation) { - if (orientation == null) return null; switch (orientation) { case DeviceOrientation.portraitUp: return 'portraitUp'; @@ -33,7 +32,6 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { /// Returns the device orientation for a given String. DeviceOrientation deserializeDeviceOrientation(String str) { - if (str == null) return null; switch (str) { case "portraitUp": return DeviceOrientation.portraitUp; 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 index 94cfc190305a..9781b593f1e8 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -117,7 +117,7 @@ void main() { // Act & Assert expect( - () => cameraPlatform.lockCaptureOrientation(1), + () => cameraPlatform.lockCaptureOrientation(1, null), throwsUnimplementedError, ); }); 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 index bf9430fd6079..bcd5995a74a0 100644 --- 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 @@ -803,14 +803,11 @@ void main() { ); // Act - await camera.lockCaptureOrientation(cameraId); await camera.lockCaptureOrientation( cameraId, DeviceOrientation.portraitUp); // Assert expect(channel.log, [ - isMethodCall('lockCaptureOrientation', - arguments: {'cameraId': cameraId, 'orientation': null}), isMethodCall('lockCaptureOrientation', arguments: {'cameraId': cameraId, 'orientation': 'portraitUp'}), ]); diff --git a/packages/camera/camera_platform_interface/test/utils/utils_test.dart b/packages/camera/camera_platform_interface/test/utils/utils_test.dart index dc9163877d57..63e3baff265d 100644 --- a/packages/camera/camera_platform_interface/test/utils/utils_test.dart +++ b/packages/camera/camera_platform_interface/test/utils/utils_test.dart @@ -32,7 +32,6 @@ void main() { }); test("serializeDeviceOrientation() should serialize correctly", () { - expect(serializeDeviceOrientation(null), null); expect(serializeDeviceOrientation(DeviceOrientation.portraitUp), "portraitUp"); expect(serializeDeviceOrientation(DeviceOrientation.portraitDown), @@ -44,7 +43,6 @@ void main() { }); test("deserializeDeviceOrientation() should deserialize correctly", () { - expect(deserializeDeviceOrientation(null), null); expect(deserializeDeviceOrientation('portraitUp'), DeviceOrientation.portraitUp); expect(deserializeDeviceOrientation('portraitDown'), From a8a9d43454b303307e14e5bba163019f0368e745 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 15:03:19 +0100 Subject: [PATCH 13/25] Add capture orientation locking (iOS done, Android WIP) --- .../io/flutter/plugins/camera/Camera.java | 1864 +++++++++-------- .../flutter/plugins/camera/CameraUtils.java | 256 ++- .../camera/DeviceOrientationListener.java | 187 +- .../plugins/camera/MethodCallHandlerImpl.java | 24 + .../plugins/camera/CameraUtilsTest.java | 152 +- packages/camera/camera/example/lib/main.dart | 22 + .../camera/camera/ios/Classes/CameraPlugin.m | 166 +- .../camera/lib/src/camera_controller.dart | 40 +- .../camera/camera/lib/src/camera_preview.dart | 25 +- packages/camera/camera/pubspec.yaml | 1 + packages/camera/camera/test/camera_test.dart | 100 + 11 files changed, 1602 insertions(+), 1235 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 87ed8162220b..2d57eef48124 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -1,8 +1,5 @@ package io.flutter.plugins.camera; -import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -38,15 +35,9 @@ import android.util.Size; import android.view.OrientationEventListener; import android.view.Surface; + import androidx.annotation.NonNull; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.PictureCaptureRequest.State; -import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.ResolutionPreset; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -59,935 +50,960 @@ import java.util.Map; import java.util.concurrent.Executors; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.PictureCaptureRequest.State; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; + +import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; +import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; + public class Camera { - private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final OrientationEventListener orientationEventListener; - private final DeviceOrientationListener deviceOrientationListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - private final Context applicationContext; - private final CamcorderProfile recordingProfile; - private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - - private CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - private CaptureRequest.Builder captureRequestBuilder; - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private File videoRecordingFile; - private int currentOrientation = ORIENTATION_UNKNOWN; - private FlashMode flashMode; - private ExposureMode exposureMode; - private PictureCaptureRequest pictureCaptureRequest; - private CameraRegions cameraRegions; - private int exposureOffset; - - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { - if (activity == null) { - throw new IllegalStateException("No activity available!"); - } - this.cameraName = cameraName; - this.enableAudio = enableAudio; - this.flutterTexture = flutterTexture; - this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - this.applicationContext = activity.getApplicationContext(); - this.flashMode = FlashMode.auto; - this.exposureMode = ExposureMode.auto; - this.exposureOffset = 0; - orientationEventListener = - new OrientationEventListener(activity.getApplicationContext()) { - @Override - public void onOrientationChanged(int i) { - if (i == ORIENTATION_UNKNOWN) { - return; + private final SurfaceTextureEntry flutterTexture; + private final CameraManager cameraManager; + private final OrientationEventListener orientationEventListener; + private final DeviceOrientationListener deviceOrientationListener; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final String cameraName; + private final Size captureSize; + private final Size previewSize; + private final boolean enableAudio; + private final Context applicationContext; + private final CamcorderProfile recordingProfile; + private final DartMessenger dartMessenger; + private final CameraZoom cameraZoom; + + private CameraDevice cameraDevice; + private CameraCaptureSession cameraCaptureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + private CaptureRequest.Builder captureRequestBuilder; + private MediaRecorder mediaRecorder; + private boolean recordingVideo; + private File videoRecordingFile; + private int currentOrientation = ORIENTATION_UNKNOWN; + private FlashMode flashMode; + private ExposureMode exposureMode; + private PictureCaptureRequest pictureCaptureRequest; + private CameraRegions cameraRegions; + private int exposureOffset; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + this.cameraName = cameraName; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + this.applicationContext = activity.getApplicationContext(); + this.flashMode = FlashMode.auto; + this.exposureMode = ExposureMode.auto; + this.exposureOffset = 0; + orientationEventListener = + new OrientationEventListener(activity.getApplicationContext()) { + @Override + public void onOrientationChanged(int i) { + if (i == ORIENTATION_UNKNOWN) { + return; + } + // Convert the raw deg angle to the nearest multiple of 90. + currentOrientation = (int) (Math.round(i / 90.0) * 90) % 360; + } + }; + orientationEventListener.enable(); + deviceOrientationListener = + new DeviceOrientationListener(activity.getApplicationContext(), dartMessenger); + deviceOrientationListener.start(); + + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + isFrontFacing = + characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; + ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); + recordingProfile = + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraName, preset); + cameraZoom = + new CameraZoom( + characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + } + + private void prepareMediaRecorder(String outputFilePath) throws IOException { + if (mediaRecorder != null) { + mediaRecorder.release(); + } + + mediaRecorder = + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation(getMediaOrientation()) + .build(); + } + + @SuppressLint("MissingPermission") + public void open() throws CameraAccessException { + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance( + previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); + + cameraManager.openCamera( + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + cameraDevice = device; + try { + cameraRegions = new CameraRegions(getRegionBoundaries()); + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + exposureMode, + isExposurePointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); + } + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + null); + } + + private void writeToFile(ByteBuffer buffer, File file) throws IOException { + try (FileOutputStream outputStream = new FileOutputStream(file)) { + while (0 < buffer.remaining()) { + outputStream.getChannel().write(buffer); } - // Convert the raw deg angle to the nearest multiple of 90. - currentOrientation = (int) (Math.round(i / 90.0) * 90) % 360; - } - }; - orientationEventListener.enable(); - deviceOrientationListener = - new DeviceOrientationListener(activity.getApplicationContext(), dartMessenger); - deviceOrientationListener.start(); - - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - isFrontFacing = - characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - cameraZoom = - new CameraZoom( - characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - } - - private void prepareMediaRecorder(String outputFilePath) throws IOException { - if (mediaRecorder != null) { - mediaRecorder.release(); - } - - mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) - .setEnableAudio(enableAudio) - .setMediaOrientation(getMediaOrientation()) - .build(); - } - - @SuppressLint("MissingPermission") - public void open() throws CameraAccessException { - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); - - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance( - previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); - - cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - cameraDevice = device; - try { - cameraRegions = new CameraRegions(getRegionBoundaries()); - startPreview(); - dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - exposureMode, - isExposurePointSupported()); - } catch (CameraAccessException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - close(); + } + } + + public void takePicture(@NonNull final Result result) { + // Only take 1 picture at a time + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } + // Store the result + this.pictureCaptureRequest = new PictureCaptureRequest(result); + + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + final File file; + try { + file = File.createTempFile("CAP", ".jpg", outputDir); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + return; + } + + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener( + reader -> { + try (Image image = reader.acquireLatestImage()) { + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + writeToFile(buffer, file); + pictureCaptureRequest.finish(file.getAbsolutePath()); + } catch (IOException e) { + pictureCaptureRequest.error("IOError", "Failed saving image", null); + } + }, + null); + + runPictureAutoFocus(); + } + + private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + processCapture(result); + } + + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + processCapture(partialResult); + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { + return; + } + String reason; + switch (failure.getReason()) { + case CaptureFailure.REASON_ERROR: + reason = "An error happened in the framework"; + break; + case CaptureFailure.REASON_FLUSHED: + reason = "The capture has failed due to an abortCaptures() call"; + break; + default: + reason = "Unknown reason"; + } + pictureCaptureRequest.error("captureFailure", reason, null); + } + + private void processCapture(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + switch (pictureCaptureRequest.getState()) { + case focusing: + if (afState == null) { + return; + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // Some devices might return null here, in which case we will also continue. + if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + runPictureCapture(); + } else { + runPicturePreCapture(); + } + } + break; + case preCapture: + // Some devices might return null here, in which case we will also continue. + if (aeState == null + || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED + || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + pictureCaptureRequest.setState(State.waitingPreCaptureReady); + } + break; + case waitingPreCaptureReady: + if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { + runPictureCapture(); + } + } + } + }; + + private void runPictureAutoFocus() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); + lockAutoFocus(); + } + + private void runPicturePreCapture() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); + + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + try { + cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + private void runPictureCapture() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); + try { + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(pictureImageReader.getSurface()); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); + switch (flashMode) { + case off: + captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case auto: + captureBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + break; + case always: + default: + captureBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + break; } - } - - @Override - public void onClosed(@NonNull CameraDevice camera) { - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } - - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - close(); - dartMessenger.sendCameraErrorEvent("The camera was disconnected."); - } - - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; + cameraCaptureSession.stopRepeating(); + cameraCaptureSession.capture( + captureBuilder.build(), + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); + } + }, + null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + private void lockAutoFocus() { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + try { + cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + private void unlockAutoFocus() { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + initPreviewCaptureBuilder(); + try { + cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); + } catch (CameraAccessException ignored) { + } + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + try { + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), pictureCaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + captureRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + captureRequestBuilder.addTarget(surface); } - dartMessenger.sendCameraErrorEvent(errorDescription); - } - }, - null); - } - - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } - } - } - - public void takePicture(@NonNull final Result result) { - // Only take 1 picture at a time - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { - result.error("captureAlreadyActive", "Picture is currently already being captured", null); - return; - } - // Store the result - this.pictureCaptureRequest = new PictureCaptureRequest(result); - - // Create temporary file - final File outputDir = applicationContext.getCacheDir(); - final File file; - try { - file = File.createTempFile("CAP", ".jpg", outputDir); - } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); - return; - } - - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener( - reader -> { - try (Image image = reader.acquireLatestImage()) { - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - writeToFile(buffer, file); - pictureCaptureRequest.finish(file.getAbsolutePath()); - } catch (IOException e) { - pictureCaptureRequest.error("IOError", "Failed saving image", null); - } - }, - null); - - runPictureAutoFocus(); - } - - private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - processCapture(result); - } - - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - processCapture(partialResult); - } - - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { + } + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + try { + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + cameraCaptureSession = session; + initPreviewCaptureBuilder(); + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + pictureCaptureCallback, + new Handler(Looper.getMainLooper())); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } + } + + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, null); + } + + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + + try { + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); return; - } - String reason; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - break; - default: - reason = "Unknown reason"; - } - pictureCaptureRequest.error("captureFailure", reason, null); } - private void processCapture(CaptureResult result) { - if (pictureCaptureRequest == null) { + try { + recordingVideo = false; + closeCaptureSession(); + mediaRecorder.stop(); + mediaRecorder.reset(); + startPreview(); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); return; - } + } - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - switch (pictureCaptureRequest.getState()) { - case focusing: - if (afState == null) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); return; - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // Some devices might return null here, in which case we will also continue. - if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - runPictureCapture(); - } else { - runPicturePreCapture(); - } - } - break; - case preCapture: - // Some devices might return null here, in which case we will also continue. - if (aeState == null - || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED - || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - pictureCaptureRequest.setState(State.waitingPreCaptureReady); - } - break; - case waitingPreCaptureReady: - if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { - runPictureCapture(); - } - } - } - }; - - private void runPictureAutoFocus() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); - lockAutoFocus(); - } - - private void runPicturePreCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); - - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - try { - cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - } - - private void runPictureCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); - try { - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); - switch (flashMode) { - case off: - captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case auto: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - break; - case always: - default: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - break; - } - cameraCaptureSession.stopRepeating(); - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); } - }, - null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - } - - private void lockAutoFocus() { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - try { - cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - } - - private void unlockAutoFocus() { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - initPreviewCaptureBuilder(); - try { - cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); - } catch (CameraAccessException ignored) { - } - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); - try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), pictureCaptureCallback, null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - } - - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); - } - } - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - try { - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); + } + + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); return; - } - cameraCaptureSession = session; - initPreviewCaptureBuilder(); - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - pictureCaptureCallback, - new Handler(Looper.getMainLooper())); - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); } - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; - - // Start the session - if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. - List configs = new ArrayList<>(); - configs.add(new OutputConfiguration(flutterSurface)); - for (Surface surface : remainingSurfaces) { - configs.add(new OutputConfiguration(surface)); - } - createCaptureSessionWithSessionConfig(configs, callback); - } else { - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - createCaptureSession(surfaceList, callback); - } - } - - @TargetApi(VERSION_CODES.P) - private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); - } - - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, null); - } - - public void startVideoRecording(Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; - } - - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - recordingVideo = false; - videoRecordingFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - recordingVideo = false; - closeCaptureSession(); - mediaRecorder.stop(); - mediaRecorder.reset(); - startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); - } - - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); - } - - public void setFlashMode(@NonNull final Result result, FlashMode mode) - throws CameraAccessException { - // Get the flash availability - Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - - // Check if flash is available. - if (flashAvailable == null || !flashAvailable) { - result.error("setFlashModeFailed", "Device does not have flash capabilities", null); - return; - } - - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { - this.flashMode = FlashMode.off; - initPreviewCaptureBuilder(); - this.cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } - updateFlash(mode); - result.success(null); - isFinished = true; - } + result.success(null); + } - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } + public void setFlashMode(@NonNull final Result result, FlashMode mode) + throws CameraAccessException { + // Get the flash availability + Boolean flashAvailable = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); - } else { - updateFlash(mode); - result.success(null); - } - } - - private void updateFlash(FlashMode mode) { - // Get flash - flashMode = mode; - initPreviewCaptureBuilder(); - try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), pictureCaptureCallback, null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - } - - public void setExposureMode(@NonNull final Result result, ExposureMode mode) - throws CameraAccessException { - this.exposureMode = mode; - initPreviewCaptureBuilder(); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - result.success(null); - } - - public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if exposure point functionality is available. - if (!isExposurePointSupported()) { - result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); - return; - } - // Check if we are doing a reset or not - if (x == null || y == null) { - x = 0.5; - y = 0.5; - } - // Get the current region boundaries. - Size maxBoundaries = getRegionBoundaries(); - if (maxBoundaries == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; - } - // Set the metering rectangle - cameraRegions.setAutoExposureMeteringRectangleFromPoint(x, y); - // Apply it - initPreviewCaptureBuilder(); - this.cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), pictureCaptureCallback, null); - result.success(null); - } - - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() throws CameraAccessException { - int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; - } - - private Size getRegionBoundaries() throws CameraAccessException { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); - } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); - } else { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - } - return rect == null ? null : new Size(rect.width(), rect.height()); - } - - private boolean isExposurePointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - return supportedRegions != null && supportedRegions > 0; - } - - public double getMinExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(); - return minStepped * stepSize; - } - - public double getMaxExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(); - return maxStepped * stepSize; - } - - public double getExposureOffsetStepSize() throws CameraAccessException { - Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - return stepSize == null ? 0.0 : stepSize.doubleValue(); - } - - public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { - // Set the exposure offset - double stepSize = getExposureOffsetStepSize(); - exposureOffset = (int) (offset / stepSize); - // Apply it - initPreviewCaptureBuilder(); - this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - result.success(offset); - } - - private void initPreviewCaptureBuilder() { - captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); - // Applying flash modes - switch (flashMode) { - case off: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case always: + // Check if flash is available. + if (flashAvailable == null || !flashAvailable) { + result.error("setFlashModeFailed", "Device does not have flash capabilities", null); + return; + } + + // If switching directly from torch to auto or on, make sure we turn off the torch. + if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { + this.flashMode = FlashMode.off; + initPreviewCaptureBuilder(); + this.cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + new CaptureCallback() { + private boolean isFinished = false; + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult captureResult) { + if (isFinished) { + return; + } + + updateFlash(mode); + result.success(null); + isFinished = true; + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (isFinished) { + return; + } + + result.error("setFlashModeFailed", "Could not set flash mode.", null); + isFinished = true; + } + }, + null); + } else { + updateFlash(mode); + result.success(null); + } + } + + private void updateFlash(FlashMode mode) { + // Get flash + flashMode = mode; + initPreviewCaptureBuilder(); + try { + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), pictureCaptureCallback, null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + public void setExposureMode(@NonNull final Result result, ExposureMode mode) + throws CameraAccessException { + this.exposureMode = mode; + initPreviewCaptureBuilder(); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(null); + } + + public void setExposurePoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if exposure point functionality is available. + if (!isExposurePointSupported()) { + result.error( + "setExposurePointFailed", "Device does not have exposure point capabilities", null); + return; + } + // Check if we are doing a reset or not + if (x == null || y == null) { + x = 0.5; + y = 0.5; + } + // Get the current region boundaries. + Size maxBoundaries = getRegionBoundaries(); + if (maxBoundaries == null) { + result.error("setExposurePointFailed", "Could not determine max region boundaries", null); + return; + } + // Set the metering rectangle + cameraRegions.setAutoExposureMeteringRectangleFromPoint(x, y); + // Apply it + initPreviewCaptureBuilder(); + this.cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), pictureCaptureCallback, null); + result.success(null); + } + + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() throws CameraAccessException { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) + availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; + } + + private Size getRegionBoundaries() throws CameraAccessException { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { + return cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + } + // Get the current distortion correction mode + Integer distortionCorrectionMode = + captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } else { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + } + return rect == null ? null : new Size(rect.width(), rect.height()); + } + + private boolean isExposurePointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + return supportedRegions != null && supportedRegions > 0; + } + + public double getMinExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; + } + + public double getMaxExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; + } + + public double getExposureOffsetStepSize() throws CameraAccessException { + Rational stepSize = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + return stepSize == null ? 0.0 : stepSize.doubleValue(); + } + + public void setExposureOffset(@NonNull final Result result, double offset) + throws CameraAccessException { + // Set the exposure offset + double stepSize = getExposureOffsetStepSize(); + exposureOffset = (int) (offset / stepSize); + // Apply it + initPreviewCaptureBuilder(); + this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(offset); + } + + + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } + + private void initPreviewCaptureBuilder() { + captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); + // Applying flash modes + switch (flashMode) { + case off: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case auto: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case always: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case torch: + default: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + break; + } + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case torch: - default: + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[]{cameraRegions.getAEMeteringRectangle()}); + switch (exposureMode) { + case locked: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + // Applying auto focus captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); - break; - } - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); - switch (exposureMode) { - case locked: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; - } - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - // Applying auto focus - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - } - - public void startPreview() throws CameraAccessException { - if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; - - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); - } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - }); - } - - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - imageStreamSink.success(imageBuffer); - img.close(); - }, - null); - } - - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; - } - - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; - } - - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; - - if (zoom > maxZoom || zoom < minZoom) { - String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); - result.error("ZOOM_ERROR", errorMessage, null); - return; - } - - //Zoom area is calculated relative to sensor area (activeRect) - if (captureRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - } - - result.success(null); - } - - private void closeCaptureSession() { - if (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; - } - } - - public void close() { - closeCaptureSession(); - - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; - } - } - - public void dispose() { - close(); - flutterTexture.release(); - orientationEventListener.disable(); - deviceOrientationListener.stop(); - } - - private int getMediaOrientation() { - final int sensorOrientationOffset = - (currentOrientation == ORIENTATION_UNKNOWN) - ? 0 - : (isFrontFacing) ? -currentOrientation : currentOrientation; - return (sensorOrientationOffset + sensorOrientation + 360) % 360; - } + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + } + + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + } + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + imageStreamSink.success(imageBuffer); + img.close(); + }, + null); + } + + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; + } + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; + } + + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; + } + + //Zoom area is calculated relative to sensor area (activeRect) + if (captureRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + } + + result.success(null); + } + + private void closeCaptureSession() { + if (cameraCaptureSession != null) { + cameraCaptureSession.close(); + cameraCaptureSession = null; + } + } + + public void close() { + closeCaptureSession(); + + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + } + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; + } + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; + } + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; + } + } + + public void dispose() { + close(); + flutterTexture.release(); + orientationEventListener.disable(); + deviceOrientationListener.stop(); + } + + private int getMediaOrientation() { + final int sensorOrientationOffset = + (currentOrientation == ORIENTATION_UNKNOWN) + ? 0 + : (isFrontFacing) ? -currentOrientation : currentOrientation; + return (sensorOrientationOffset + sensorOrientation + 360) % 360; + } + } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 892c9b3cbdf9..5dc2ca78d815 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -10,8 +10,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugins.camera.types.ResolutionPreset; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -20,136 +19,161 @@ import java.util.List; import java.util.Map; -/** Provides various utilities for camera. */ -public final class CameraUtils { +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.types.ResolutionPreset; - private CameraUtils() {} +/** + * Provides various utilities for camera. + */ +public final class CameraUtils { - static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { - // Round to the nearest 90 degrees. - degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; - // Determine the corresponding device orientation. - switch (degrees) { - case 90: - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - case 180: - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - case 270: - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - case 0: - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + private CameraUtils() { } - } - static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { - assert (orientation != null); - switch (orientation) { - case PORTRAIT_UP: - return "portraitUp"; - case PORTRAIT_DOWN: - return "portraitDown"; - case LANDSCAPE_LEFT: - return "landscapeLeft"; - case LANDSCAPE_RIGHT: - return "landscapeRight"; - default: - throw new UnsupportedOperationException( - "Could not serialize device orientation: " + orientation.toString()); + static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { + // Round to the nearest 90 degrees. + degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; + // Determine the corresponding device orientation. + switch (degrees) { + case 90: + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case 180: + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case 270: + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + case 0: + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } } - } - static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; + static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { + if (orientation == null) + throw new UnsupportedOperationException("Could not serialize null device orientation."); + switch (orientation) { + case PORTRAIT_UP: + return "portraitUp"; + case PORTRAIT_DOWN: + return "portraitDown"; + case LANDSCAPE_LEFT: + return "landscapeLeft"; + case LANDSCAPE_RIGHT: + return "landscapeRight"; + default: + throw new UnsupportedOperationException( + "Could not serialize device orientation: " + orientation.toString()); + } } - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } + static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { + if (orientation == null) + throw new UnsupportedOperationException("Could not deserialize null device orientation."); + switch (orientation) { + case "portraitUp": + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + case "portraitDown": + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case "landscapeLeft": + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case "landscapeRight": + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + default: + throw new UnsupportedOperationException( + "Could not deserialize device orientation: " + orientation); + } + } - static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { - // For still image captures, we use the largest available size. - return Collections.max( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); - } + static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; + } - public static List> getAvailableCameras(Activity activity) - throws CameraAccessException { - CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - String[] cameraNames = cameraManager.getCameraIdList(); - List> cameras = new ArrayList<>(); - for (String cameraName : cameraNames) { - HashMap details = new HashMap<>(); - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - details.put("name", cameraName); - int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - details.put("sensorOrientation", sensorOrientation); + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } - int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); - switch (lensFacing) { - case CameraMetadata.LENS_FACING_FRONT: - details.put("lensFacing", "front"); - break; - case CameraMetadata.LENS_FACING_BACK: - details.put("lensFacing", "back"); - break; - case CameraMetadata.LENS_FACING_EXTERNAL: - details.put("lensFacing", "external"); - break; - } - cameras.add(details); + static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { + // For still image captures, we use the largest available size. + return Collections.max( + Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), + new CompareSizesByArea()); } - return cameras; - } - static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - String cameraName, ResolutionPreset preset) { - int cameraId = Integer.parseInt(cameraName); - switch (preset) { - // All of these cases deliberately fall through to get the best available profile. - case max: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); - } - case ultraHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); - } - case veryHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); - } - case high: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); - } - case medium: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); - } - case low: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + public static List> getAvailableCameras(Activity activity) + throws CameraAccessException { + CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + String[] cameraNames = cameraManager.getCameraIdList(); + List> cameras = new ArrayList<>(); + for (String cameraName : cameraNames) { + HashMap details = new HashMap<>(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + details.put("name", cameraName); + int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + details.put("sensorOrientation", sensorOrientation); + + int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); + switch (lensFacing) { + case CameraMetadata.LENS_FACING_FRONT: + details.put("lensFacing", "front"); + break; + case CameraMetadata.LENS_FACING_BACK: + details.put("lensFacing", "back"); + break; + case CameraMetadata.LENS_FACING_EXTERNAL: + details.put("lensFacing", "external"); + break; + } + cameras.add(details); } - default: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); - } else { - throw new IllegalArgumentException( - "No capture session available for current capture session."); + return cameras; + } + + static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + String cameraName, ResolutionPreset preset) { + int cameraId = Integer.parseInt(cameraName); + switch (preset) { + // All of these cases deliberately fall through to get the best available profile. + case max: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); + } else { + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } } } - } - private static class CompareSizesByArea implements Comparator { - @Override - public int compare(Size lhs, Size rhs) { - // We cast here to ensure the multiplications won't overflow. - return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + private static class CompareSizesByArea implements Comparator { + @Override + public int compare(Size lhs, Size rhs) { + // We cast here to ensure the multiplications won't overflow. + return Long.signum( + (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + } } - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java index e85205bcd2bc..da064a815bf5 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java @@ -5,70 +5,147 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; +import android.hardware.SensorManager; +import android.provider.Settings; +import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; + import io.flutter.embedding.engine.systemchannels.PlatformChannel; class DeviceOrientationListener { - private static final IntentFilter orientationIntentFilter = - new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - - private final Context context; - private BroadcastReceiver broadcastReceiver; - private DartMessenger messenger; - private PlatformChannel.DeviceOrientation lastOrientation; - - public DeviceOrientationListener(Context context, DartMessenger messenger) { - this.context = context; - this.messenger = messenger; - } - - public void start() { - if (broadcastReceiver != null) return; - - broadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - PlatformChannel.DeviceOrientation orientation = getOrientation(); - if (orientation == null || lastOrientation == orientation) return; - lastOrientation = orientation; - messenger.sendDeviceOrientationChangeEvent(orientation); - } + + private static final IntentFilter orientationIntentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Context context; + private final DartMessenger messenger; + private PlatformChannel.DeviceOrientation lastOrientation; + private OrientationEventListener orientationEventListener; + private BroadcastReceiver broadcastReceiver; + + + public DeviceOrientationListener(Context context, DartMessenger messenger) { + this.context = context; + this.messenger = messenger; + } + + public void start() { + startSensorListener(); + startUIListener(); + } + + public void stop() { + stopSensorListener(); + stopUIListener(); + } + + private void startSensorListener() { + if (orientationEventListener != null) return; + orientationEventListener = new OrientationEventListener(context, SensorManager.SENSOR_DELAY_NORMAL) { + @Override + public void onOrientationChanged(int angle) { + if (!isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); + if (!newOrientation.equals(lastOrientation)) { + lastOrientation = newOrientation; + messenger.sendDeviceOrientationChangeEvent(newOrientation); + } + } + } }; + if (orientationEventListener.canDetectOrientation()) { + orientationEventListener.enable(); + } + } - context.registerReceiver(broadcastReceiver, orientationIntentFilter); - // Trigger initial value - broadcastReceiver.onReceive(context, null); - } - - public void stop() { - if (broadcastReceiver == null) return; - context.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - private PlatformChannel.DeviceOrientation getOrientation() { - final int rotation = - ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay() - .getRotation(); - final int orientation = context.getResources().getConfiguration().orientation; - switch (orientation) { - case Configuration.ORIENTATION_PORTRAIT: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } else { - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + private void startUIListener() { + if (broadcastReceiver != null) return; + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + if (!orientation.equals(lastOrientation)) { + lastOrientation = orientation; + messenger.sendDeviceOrientationChangeEvent(orientation); + } + } + } + }; + context.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(context, null); + } + + private void stopSensorListener() { + if (orientationEventListener == null) return; + orientationEventListener.disable(); + orientationEventListener = null; + } + + private void stopUIListener() { + if (broadcastReceiver == null) return; + context.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + private boolean isSystemAutoRotationLocked() { + return android.provider.Settings.System.getInt(context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) != 1; + } + + private PlatformChannel.DeviceOrientation getUIOrientation() { + final int rotation = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation(); + final int orientation = context.getResources().getConfiguration().orientation; + + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; } - case Configuration.ORIENTATION_LANDSCAPE: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } + + private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + final int tolerance = 45; + angle += tolerance; + + // Orientation is 0 in the default orientation mode. This is portait-mode for phones + // and landscape for tablets. We have to compensate for this by calculating the default + // orientation, and apply an offset accordingly. + int defaultDeviceOrientation = getDeviceDefaultOrientation(); + if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { + angle += 90; + } + // Determine the orientation + angle = angle % 360; + return new PlatformChannel.DeviceOrientation[]{ + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + }[angle / 90]; + } + + private int getDeviceDefaultOrientation() { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Configuration config = context.getResources().getConfiguration(); + int rotation = windowManager.getDefaultDisplay().getRotation(); + if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) && + config.orientation == Configuration.ORIENTATION_LANDSCAPE) + || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) && + config.orientation == Configuration.ORIENTATION_PORTRAIT)) { + return Configuration.ORIENTATION_LANDSCAPE; } else { - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + return Configuration.ORIENTATION_PORTRAIT; } - default: - return null; } - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 78a10010f90b..fedc587da0c0 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -4,6 +4,8 @@ import android.hardware.camera2.CameraAccessException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; @@ -269,6 +271,28 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "lockCaptureOrientation": + { + PlatformChannel.DeviceOrientation orientation = CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); + + try { + camera.lockCaptureOrientation(orientation); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "unlockCaptureOrientation": + { + try { + camera.unlockCaptureOrientation(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java index 6bdea0662915..b03fea5ab751 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -1,72 +1,98 @@ package io.flutter.plugins.camera; -import static org.junit.Assert.assertEquals; +import org.junit.Test; import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import org.junit.Test; + +import static org.junit.Assert.assertEquals; public class CameraUtilsTest { - @Test - public void serializeDeviceOrientation_serializes_correctly() { - assertEquals( - "portraitUp", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); - assertEquals( - "portraitDown", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); - assertEquals( - "landscapeLeft", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); - assertEquals( - "landscapeRight", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); - } + @Test + public void serializeDeviceOrientation_serializes_correctly() { + assertEquals( + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + assertEquals( + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + assertEquals( + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + assertEquals( + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + } + + @Test(expected = UnsupportedOperationException.class) + public void serializeDeviceOrientation_throws_for_null() { + CameraUtils.serializeDeviceOrientation(null); + } + + @Test + public void deserializeDeviceOrientation_deserializes_correctly() { + assertEquals(PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.deserializeDeviceOrientation("portraitUp")); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.deserializeDeviceOrientation("portraitDown")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.deserializeDeviceOrientation("landscapeLeft")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.deserializeDeviceOrientation("landscapeRight")); + } + + @Test(expected = UnsupportedOperationException.class) + public void deserializeDeviceOrientation_throws_for_null() { + CameraUtils.deserializeDeviceOrientation(null); + } - @Test - public void getDeviceOrientationFromDegrees_converts_correctly() { - // Portrait UP - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(0)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(315)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(44)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(-45)); - // Portrait DOWN - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(180)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(135)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(224)); - // Landscape LEFT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(90)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(45)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(134)); - // Landscape RIGHT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(270)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(225)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(314)); - } + @Test + public void getDeviceOrientationFromDegrees_converts_correctly() { + // Portrait UP + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(0)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(315)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(44)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(-45)); + // Portrait DOWN + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(180)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(135)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(224)); + // Landscape LEFT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(90)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(45)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(134)); + // Landscape RIGHT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(270)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(225)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(314)); + } } diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 9bb0321b7746..1a7683f2b398 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -254,6 +254,15 @@ class _CameraExampleHomeState extends State color: Colors.blue, onPressed: controller != null ? onAudioModeButtonPressed : null, ), + IconButton( + icon: Icon(controller?.value?.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: controller != null + ? onCaptureOrientationLockButtonPressed + : null, + ), ], ), _flashModeControlRowWidget(), @@ -545,6 +554,19 @@ class _CameraExampleHomeState extends State } } + void onCaptureOrientationLockButtonPressed() async { + if (controller != null) { + if (controller.value.isCaptureOrientationLocked) { + await controller.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await controller.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${controller.value.lockedCaptureOrientation.toString().split('.').last}'); + } + } + } + void onSetFlashModeButtonPressed(FlashMode mode) { setFlashMode(mode).then((_) { if (mounted) setState(() {}); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 958a28c3a30a..75b98b079fc2 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -18,8 +18,6 @@ @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @property(readonly, nonatomic) FlutterResult result; -@property(readonly, nonatomic) AVCaptureDevicePosition cameraPosition; -@property(readonly, nonatomic) int deviceRotation; @end @interface FLTImageStreamHandler : NSObject @@ -45,15 +43,10 @@ @implementation FLTSavePhotoDelegate { FLTSavePhotoDelegate *selfReference; } -- initWithPath:(NSString *)path - result:(FlutterResult)result - cameraPosition:(AVCaptureDevicePosition)cameraPosition - deviceRotation:(int)deviceRotation { +- initWithPath:(NSString *)path result:(FlutterResult)result { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - _cameraPosition = cameraPosition; - _deviceRotation = deviceRotation; selfReference = self; _result = result; return self; @@ -186,6 +179,42 @@ static ExposureMode getExposureModeForString(NSString *mode) { } } +static UIDeviceOrientation getUIDeviceOrientationForString(NSString *orientation) { + if ([orientation isEqualToString:@"portraitDown"]) { + return UIDeviceOrientationPortraitUpsideDown; + } else if ([orientation isEqualToString:@"landscapeLeft"]) { + return UIDeviceOrientationLandscapeRight; + } else if ([orientation isEqualToString:@"landscapeRight"]) { + return UIDeviceOrientationLandscapeLeft; + } else if ([orientation isEqualToString:@"portraitUp"]) { + return UIDeviceOrientationPortrait; + } else { + NSError *error = [NSError + errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : + [NSString stringWithFormat:@"Unknown device orientation %@", orientation] + }]; + @throw error; + } +} + +static NSString *getStringForUIDeviceOrientation(UIDeviceOrientation orientation) { + switch (orientation) { + case UIDeviceOrientationPortraitUpsideDown: + return @"portraitDown"; + case UIDeviceOrientationLandscapeRight: + return @"landscapeLeft"; + case UIDeviceOrientationLandscapeLeft: + return @"landscapeRight"; + case UIDeviceOrientationPortrait: + default: + return @"portraitUp"; + break; + }; +} + // Mirrors ResolutionPreset in camera.dart typedef enum { veryLow, @@ -255,6 +284,7 @@ @interface FLTCam : NSObject lockedCaptureOrientation != null; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -123,6 +131,7 @@ class CameraValue { ExposureMode exposureMode, bool exposurePointSupported, DeviceOrientation deviceOrientation, + Optional lockedCaptureOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -137,6 +146,9 @@ class CameraValue { exposurePointSupported: exposurePointSupported ?? this.exposurePointSupported, deviceOrientation: deviceOrientation ?? this.deviceOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, ); } @@ -151,7 +163,8 @@ class CameraValue { 'flashMode: $flashMode, ' 'exposureMode: $exposureMode, ' 'exposurePointSupported: $exposurePointSupported, ' - 'deviceOrientation: $deviceOrientation)'; + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation)'; } } @@ -706,6 +719,31 @@ class CameraController extends ValueNotifier { } } + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation([DeviceOrientation orientation]) async { + try { + await CameraPlatform.instance.lockCaptureOrientation( + _cameraId, orientation ?? value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: + Optional.fromNullable(orientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation() async { + try { + await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith(lockedCaptureOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Releases the resources of this camera. @override Future dispose() async { diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 4ee931a66a9a..6f2f9544ca38 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -23,18 +23,25 @@ class CameraPreview extends StatelessWidget { Widget build(BuildContext context) { return controller.value.isInitialized ? AspectRatio( - aspectRatio: _isLandscape() - ? controller.value.aspectRatio - : (1 / controller.value.aspectRatio), + aspectRatio: _isLandscape() ? controller.value.aspectRatio : (1 / controller.value.aspectRatio), child: Stack( fit: StackFit.expand, children: [ RotatedBox( quarterTurns: _getQuarterTurns(), - child: - CameraPlatform.instance.buildPreview(controller.cameraId), + child: CameraPlatform.instance.buildPreview(controller.cameraId), ), child ?? Container(), + Container(color: Colors.white.withOpacity(0.5)), + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(controller.value.deviceOrientation.toString()), + Text(controller.value.lockedCaptureOrientation?.toString() ?? 'NO LOCK'), + ], + ), + ), ], ), ) @@ -43,17 +50,17 @@ class CameraPreview extends StatelessWidget { bool _isLandscape() { return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] - .contains(controller.value.deviceOrientation); + .contains(controller.value.lockedCaptureOrientation ?? controller.value.deviceOrientation); } int _getQuarterTurns() { int platformOffset = defaultTargetPlatform == TargetPlatform.iOS ? 1 : 0; Map turns = { DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeLeft: 1, DeviceOrientation.portraitDown: 2, - DeviceOrientation.landscapeRight: 1, - DeviceOrientation.landscapeLeft: 3 + DeviceOrientation.landscapeRight: 3, }; - return turns[controller.value.deviceOrientation] + platformOffset; + return turns[controller.value.lockedCaptureOrientation ?? controller.value.deviceOrientation] + platformOffset; } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 834bd8e1276c..f7ad06bf765a 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: camera_platform_interface: path: ../camera_platform_interface pedantic: ^1.8.0 + quiver: ^2.1.5 dev_dependencies: path_provider: ^0.5.0 diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index f4d5eeca60e9..b0b470cf1223 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -1000,6 +1000,106 @@ void main() { .setExposureOffset(cameraController.cameraId, -0.4)) .called(4); }); + + test('lockCaptureOrientation() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.lockCaptureOrientation(); + expect(cameraController.value.lockedCaptureOrientation, + equals(DeviceOrientation.portraitUp)); + await cameraController + .lockCaptureOrientation(DeviceOrientation.landscapeRight); + expect(cameraController.value.lockedCaptureOrientation, + equals(DeviceOrientation.landscapeRight)); + + verify(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.portraitUp)) + .called(1); + verify(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.landscapeRight)) + .called(1); + }); + + test( + 'lockCaptureOrientation() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.portraitUp)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('unlockCaptureOrientation() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.unlockCaptureOrientation(); + expect(cameraController.value.lockedCaptureOrientation, equals(null)); + + verify(CameraPlatform.instance + .unlockCaptureOrientation(cameraController.cameraId)) + .called(1); + }); + + test( + 'unlockCaptureOrientation() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .unlockCaptureOrientation(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.unlockCaptureOrientation(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); }); } From 3e9aad74e9a4d02bec6a3b325b0f9b9051b88e0d Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 15:43:42 +0100 Subject: [PATCH 14/25] Add orientation lock to android implementation --- .../io/flutter/plugins/camera/Camera.java | 45 ++++++------------- ...ner.java => DeviceOrientationManager.java} | 33 +++++++++++++- .../camera/camera/test/camera_value_test.dart | 31 ++++++++----- 3 files changed, 65 insertions(+), 44 deletions(-) rename packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/{DeviceOrientationListener.java => DeviceOrientationManager.java} (86%) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 540be3ce4f54..76ecf6b70a8a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -1,6 +1,5 @@ package io.flutter.plugins.camera; -import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; import android.annotation.SuppressLint; @@ -36,7 +35,6 @@ import android.util.Range; import android.util.Rational; import android.util.Size; -import android.view.OrientationEventListener; import android.view.Surface; import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.PlatformChannel; @@ -63,8 +61,7 @@ public class Camera { private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; - private final OrientationEventListener orientationEventListener; - private final DeviceOrientationListener deviceOrientationListener; + private final DeviceOrientationManager deviceOrientationListener; private final boolean isFrontFacing; private final int sensorOrientation; private final String cameraName; @@ -84,7 +81,6 @@ public class Camera { private MediaRecorder mediaRecorder; private boolean recordingVideo; private File videoRecordingFile; - private int currentOrientation = ORIENTATION_UNKNOWN; private FlashMode flashMode; private ExposureMode exposureMode; private PictureCaptureRequest pictureCaptureRequest; @@ -112,21 +108,6 @@ public Camera( this.flashMode = FlashMode.auto; this.exposureMode = ExposureMode.auto; this.exposureOffset = 0; - orientationEventListener = - new OrientationEventListener(activity.getApplicationContext()) { - @Override - public void onOrientationChanged(int i) { - if (i == ORIENTATION_UNKNOWN) { - return; - } - // Convert the raw deg angle to the nearest multiple of 90. - currentOrientation = (int) (Math.round(i / 90.0) * 90) % 360; - } - }; - orientationEventListener.enable(); - deviceOrientationListener = - new DeviceOrientationListener(activity.getApplicationContext(), dartMessenger); - deviceOrientationListener.start(); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); @@ -141,6 +122,10 @@ public void onOrientationChanged(int i) { new CameraZoom( characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + deviceOrientationListener = + new DeviceOrientationManager( + activity.getApplicationContext(), dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); } private void prepareMediaRecorder(String outputFilePath) throws IOException { @@ -151,7 +136,10 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { mediaRecorder = new MediaRecorderBuilder(recordingProfile, outputFilePath) .setEnableAudio(enableAudio) - .setMediaOrientation(getMediaOrientation()) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) .build(); } @@ -378,7 +366,11 @@ private void runPictureCapture() { final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); + captureBuilder.set( + CaptureRequest.JPEG_ORIENTATION, + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)); switch (flashMode) { case off: captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); @@ -989,15 +981,6 @@ public void close() { public void dispose() { close(); flutterTexture.release(); - orientationEventListener.disable(); deviceOrientationListener.stop(); } - - private int getMediaOrientation() { - final int sensorOrientationOffset = - (currentOrientation == ORIENTATION_UNKNOWN) - ? 0 - : (isFrontFacing) ? -currentOrientation : currentOrientation; - return (sensorOrientationOffset + sensorOrientation + 360) % 360; - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java similarity index 86% rename from packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index f4ccb8b828ff..c1ef2303f80d 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationListener.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -12,20 +12,25 @@ import android.view.WindowManager; import io.flutter.embedding.engine.systemchannels.PlatformChannel; -class DeviceOrientationListener { +class DeviceOrientationManager { private static final IntentFilter orientationIntentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); private final Context context; private final DartMessenger messenger; + private final boolean isFrontFacing; + private final int sensorOrientation; private PlatformChannel.DeviceOrientation lastOrientation; private OrientationEventListener orientationEventListener; private BroadcastReceiver broadcastReceiver; - public DeviceOrientationListener(Context context, DartMessenger messenger) { + public DeviceOrientationManager( + Context context, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { this.context = context; this.messenger = messenger; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; } public void start() { @@ -38,6 +43,30 @@ public void stop() { stopUIListener(); } + public int getMediaOrientation() { + return this.getMediaOrientation(this.lastOrientation); + } + + public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + switch (orientation) { + case PORTRAIT_UP: + angle = 0; + break; + case PORTRAIT_DOWN: + angle = 180; + break; + case LANDSCAPE_LEFT: + angle = 90; + break; + case LANDSCAPE_RIGHT: + angle = 270; + break; + } + if (isFrontFacing) angle *= -1; + return (angle + sensorOrientation + 360) % 360; + } + private void startSensorListener() { if (orientationEventListener != null) return; orientationEventListener = diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 1c976bdf30c0..bdba7ae2cf80 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -14,16 +14,19 @@ void main() { group('camera_value', () { test('Can be created', () { var cameraValue = const CameraValue( - isInitialized: false, - errorDescription: null, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - exposurePointSupported: true); + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + ); expect(cameraValue, isA()); expect(cameraValue.isInitialized, isFalse); @@ -36,6 +39,9 @@ void main() { expect(cameraValue.flashMode, FlashMode.auto); expect(cameraValue.exposureMode, ExposureMode.auto); expect(cameraValue.exposurePointSupported, true); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect( + cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); }); test('Can be created as uninitialized', () { @@ -52,6 +58,8 @@ void main() { expect(cameraValue.flashMode, FlashMode.auto); expect(cameraValue.exposureMode, null); expect(cameraValue.exposurePointSupported, false); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.lockedCaptureOrientation, null); }); test('Can be copied with isInitialized', () { @@ -109,10 +117,11 @@ void main() { flashMode: FlashMode.auto, exposurePointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: null, exposurePointSupported: true, deviceOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: null, exposurePointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp)'); }); }); } From 118bd001b6bc8e5856e2f65ff7f026c01b22171b Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 15:47:04 +0100 Subject: [PATCH 15/25] Update comment --- .../lib/src/platform_interface/camera_platform.dart | 4 ++++ 1 file changed, 4 insertions(+) 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 index a6a93b98e181..16a4543fc6a5 100644 --- 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 @@ -81,6 +81,10 @@ abstract class CameraPlatform extends PlatformInterface { } /// The device orientation changed. + /// + /// Implementations for this: + /// - Should support all 4 orientations. + /// - Should not emit new values when the screen orientation is locked. Stream onDeviceOrientationChanged() { throw UnimplementedError( 'onDeviceOrientationChanged() is not implemented.'); From aa2c3924067a0ca8e8da0b49d58e9a84a3f2f3c2 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 15:58:38 +0100 Subject: [PATCH 16/25] Maintain preview rotation while recording --- .../camera/lib/src/camera_controller.dart | 22 +++++++++++++--- .../camera/camera/lib/src/camera_preview.dart | 26 +++++++------------ .../camera/camera/test/camera_value_test.dart | 9 ++++++- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 0db4a6f6067b..8cdcc5de7d85 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -44,6 +44,7 @@ class CameraValue { this.exposurePointSupported, this.deviceOrientation, this.lockedCaptureOrientation, + this.recordingOrientation, }) : _isRecordingPaused = isRecordingPaused; /// Creates a new camera controller state for an uninitialized controller. @@ -115,6 +116,9 @@ class CameraValue { /// Whether the capture orientation is currently locked. bool get isCaptureOrientationLocked => lockedCaptureOrientation != null; + /// The orientation of the currently running video recording. + final DeviceOrientation recordingOrientation; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -132,6 +136,7 @@ class CameraValue { bool exposurePointSupported, DeviceOrientation deviceOrientation, Optional lockedCaptureOrientation, + Optional recordingOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -149,6 +154,9 @@ class CameraValue { lockedCaptureOrientation: lockedCaptureOrientation == null ? this.lockedCaptureOrientation : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, ); } @@ -164,7 +172,8 @@ class CameraValue { 'exposureMode: $exposureMode, ' 'exposurePointSupported: $exposurePointSupported, ' 'deviceOrientation: $deviceOrientation, ' - 'lockedCaptureOrientation: $lockedCaptureOrientation)'; + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation)'; } } @@ -432,7 +441,11 @@ class CameraController extends ValueNotifier { try { await CameraPlatform.instance.startVideoRecording(_cameraId); - value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + recordingOrientation: Optional.fromNullable( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -456,7 +469,10 @@ class CameraController extends ValueNotifier { } try { XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); - value = value.copyWith(isRecordingVideo: false); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: Optional.absent(), + ); return file; } on PlatformException catch (e) { throw CameraException(e.code, e.message); diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 2d5f31658888..05e969004233 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -35,28 +35,22 @@ class CameraPreview extends StatelessWidget { CameraPlatform.instance.buildPreview(controller.cameraId), ), child ?? Container(), - Container(color: Colors.white.withOpacity(0.5)), - Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(controller.value.deviceOrientation.toString()), - Text(controller.value.lockedCaptureOrientation - ?.toString() ?? - 'NO LOCK'), - ], - ), - ), ], ), ) : Container(); } + DeviceOrientation _getApplicableOrientation() { + return controller.value.isRecordingVideo + ? controller.value.recordingOrientation + : (controller.value.lockedCaptureOrientation ?? + controller.value.deviceOrientation); + } + bool _isLandscape() { return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] - .contains(controller.value.lockedCaptureOrientation ?? - controller.value.deviceOrientation); + .contains(_getApplicableOrientation()); } int _getQuarterTurns() { @@ -67,8 +61,6 @@ class CameraPreview extends StatelessWidget { DeviceOrientation.portraitDown: 2, DeviceOrientation.landscapeRight: 3, }; - return turns[controller.value.lockedCaptureOrientation ?? - controller.value.deviceOrientation] + - platformOffset; + return turns[_getApplicableOrientation()] + platformOffset; } } diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index bdba7ae2cf80..096627de9a4e 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -26,6 +26,7 @@ void main() { exposurePointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue, isA()); @@ -42,6 +43,7 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect( cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); }); test('Can be created as uninitialized', () { @@ -60,6 +62,7 @@ void main() { expect(cameraValue.exposurePointSupported, false); expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); + expect(cameraValue.recordingOrientation, null); }); test('Can be copied with isInitialized', () { @@ -77,6 +80,9 @@ void main() { expect(cameraValue.flashMode, FlashMode.auto); expect(cameraValue.exposureMode, null); expect(cameraValue.exposurePointSupported, false); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.lockedCaptureOrientation, null); + expect(cameraValue.recordingOrientation, null); }); test('Has aspectRatio after setting size', () { @@ -118,10 +124,11 @@ void main() { exposurePointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: null, exposurePointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: null, exposurePointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); }); }); } From 0bb07beab4bac45ace7e6a924c0d961f6d159053 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 16:22:38 +0100 Subject: [PATCH 17/25] Update comment. --- .../camera_platform_interface/lib/src/events/camera_event.dart | 3 ++- .../camera_platform_interface/lib/src/events/device_event.dart | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 index 590713d04e8b..338a97f19d0a 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -4,7 +4,8 @@ import '../../camera_platform_interface.dart'; -/// Generic Event coming from the native side of Camera. +/// Generic Event coming from the native side of Camera, +/// related to a specific camera module. /// /// All [CameraEvent]s contain the `cameraId` that originated the event. This /// should never be `null`. diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart index 68728db02af6..96857b163292 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -1,7 +1,8 @@ import 'package:camera_platform_interface/src/utils/utils.dart'; import 'package:flutter/services.dart'; -/// Generic Event coming from the native side of Camera, that are not related to a specific camera module. +/// Generic Event coming from the native side of Camera, +/// not related to a specific camera module. /// /// This class is used as a base class for all the events that might be /// triggered from a device, but it is never used directly as an event type. From a2c3fbf3b4ed51f48a485b74132ca403157c8bb5 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 16:25:13 +0100 Subject: [PATCH 18/25] Update changelog and pubspec version --- packages/camera/camera_platform_interface/CHANGELOG.md | 5 +++++ packages/camera/camera_platform_interface/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index d264d6c6f9ce..b7739e6fc5a8 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.4.0 + +- Introduces interface methods for locking and unlocking the capture orientation. +- Introduces interface method for listening to the device orientation. + ## 1.3.0 - Introduces an option to set the image format when initializing. diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 7a4fa49ce052..a6d0b815e660 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ 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.3.0 +version: 1.4.0 dependencies: flutter: From 9242af087159a2c7e1acb74b525e5cfb6e89194c Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Tue, 5 Jan 2021 16:45:45 +0100 Subject: [PATCH 19/25] Updated changelog and pubspec version --- packages/camera/camera/CHANGELOG.md | 8 ++++++++ packages/camera/camera/pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 3bb1b0639c97..168e78a85a3e 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.7.0 + +* Added support for capture orientation locking on Android and iOS. +* Fixed camera preview not rotating correctly on Android and iOS. +* Fixed camera preview sometimes appearing stretched on Android and iOS. +* Fixed videos & photos saving with the incorrect rotation on iOS. +* BREAKING CHANGE: `CameraValue.aspectRatio` now returns `width / height` rather than `height / width`. + ## 0.6.4+1 * Added closeCaptureSession() to stopVideoRecording in Camera.java to fix an Android 6 crash diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index f7ad06bf765a..0caddc294cfb 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,7 +2,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.6.4+1 +version: 0.7.0 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: From 1d409b992040bac9e5b3d1378e3b421feb8d22ef Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Tue, 5 Jan 2021 23:46:59 +0100 Subject: [PATCH 20/25] Update packages/camera/camera_platform_interface/lib/src/events/device_event.dart Co-authored-by: Maurits van Beusekom --- .../lib/src/events/device_event.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart index 96857b163292..81f3287c3706 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -1,3 +1,7 @@ +// 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/src/utils/utils.dart'; import 'package:flutter/services.dart'; From e69f746318e8cd2120740a387dc66a505cb90892 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 6 Jan 2021 11:20:27 +0100 Subject: [PATCH 21/25] Fix formatting --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index af10422bb0c9..976dd96e9dd9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -854,7 +854,7 @@ public void setExposureOffset(@NonNull final Result result, double offset) this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); result.success(offset); } - + public float getMaxZoomLevel() { return cameraZoom.maxZoom; } From 1d84baea878119e0f5e0443708c19bd6748f9f36 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 6 Jan 2021 15:10:38 +0100 Subject: [PATCH 22/25] Fix deprecation warning --- packages/camera/camera/android/build.gradle | 2 +- .../io/flutter/plugins/camera/Camera.java | 3 +- .../camera/DeviceOrientationManager.java | 42 ++++++++++++------- .../camera/example/android/app/build.gradle | 2 +- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 0b88fd10fb71..0606738a0a69 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -27,7 +27,7 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 976dd96e9dd9..0f706abf8d1e 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -136,8 +136,7 @@ public Camera( cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); deviceOrientationListener = - new DeviceOrientationManager( - activity.getApplicationContext(), dartMessenger, isFrontFacing, sensorOrientation); + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); deviceOrientationListener.start(); } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index c1ef2303f80d..c969fb49fe63 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -1,12 +1,16 @@ package io.flutter.plugins.camera; +import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.hardware.SensorManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.provider.Settings; +import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; @@ -17,7 +21,7 @@ class DeviceOrientationManager { private static final IntentFilter orientationIntentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - private final Context context; + private final Activity activity; private final DartMessenger messenger; private final boolean isFrontFacing; private final int sensorOrientation; @@ -26,8 +30,8 @@ class DeviceOrientationManager { private BroadcastReceiver broadcastReceiver; public DeviceOrientationManager( - Context context, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { - this.context = context; + Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { + this.activity = activity; this.messenger = messenger; this.isFrontFacing = isFrontFacing; this.sensorOrientation = sensorOrientation; @@ -70,7 +74,7 @@ public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { private void startSensorListener() { if (orientationEventListener != null) return; orientationEventListener = - new OrientationEventListener(context, SensorManager.SENSOR_DELAY_NORMAL) { + new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { @Override public void onOrientationChanged(int angle) { if (!isSystemAutoRotationLocked()) { @@ -102,8 +106,8 @@ public void onReceive(Context context, Intent intent) { } } }; - context.registerReceiver(broadcastReceiver, orientationIntentFilter); - broadcastReceiver.onReceive(context, null); + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); } private void stopSensorListener() { @@ -114,22 +118,19 @@ private void stopSensorListener() { private void stopUIListener() { if (broadcastReceiver == null) return; - context.unregisterReceiver(broadcastReceiver); + activity.unregisterReceiver(broadcastReceiver); broadcastReceiver = null; } private boolean isSystemAutoRotationLocked() { return android.provider.Settings.System.getInt( - context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) + activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) != 1; } private PlatformChannel.DeviceOrientation getUIOrientation() { - final int rotation = - ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay() - .getRotation(); - final int orientation = context.getResources().getConfiguration().orientation; + final int rotation = getDisplay().getRotation(); + final int orientation = activity.getResources().getConfiguration().orientation; switch (orientation) { case Configuration.ORIENTATION_PORTRAIT: @@ -172,9 +173,8 @@ private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) } private int getDeviceDefaultOrientation() { - WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - Configuration config = context.getResources().getConfiguration(); - int rotation = windowManager.getDefaultDisplay().getRotation(); + Configuration config = activity.getResources().getConfiguration(); + int rotation = getDisplay().getRotation(); if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) && config.orientation == Configuration.ORIENTATION_LANDSCAPE) || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) @@ -184,4 +184,14 @@ private int getDeviceDefaultOrientation() { return Configuration.ORIENTATION_PORTRAIT; } } + + @SuppressWarnings("deprecation") + private Display getDisplay() { + if (VERSION.SDK_INT >= VERSION_CODES.R) { + return activity.getDisplay(); + } else { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + } + } } diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index 7d0e281b74e8..c5eeb246fe30 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 lintOptions { disable 'InvalidPackage' From 3ca8eaf78ade9588e0bcf26023f35d403ced8c77 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Mon, 11 Jan 2021 11:06:36 +0100 Subject: [PATCH 23/25] Update platform interface dependency --- packages/camera/camera/pubspec.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 0caddc294cfb..73d56fef6efa 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -8,9 +8,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter - # TODO (BeMacized): Change back to version reference when the platform interface updates have been published. - camera_platform_interface: - path: ../camera_platform_interface + camera_platform_interface: ^1.5.0 pedantic: ^1.8.0 quiver: ^2.1.5 From 313595345ed7cdc79ea1e041c984faa55a9595ff Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 13 Jan 2021 19:31:25 +0100 Subject: [PATCH 24/25] Rollback update to Android compileSdkVersion 30 --- packages/camera/camera/android/build.gradle | 2 +- .../flutter/plugins/camera/DeviceOrientationManager.java | 9 +-------- packages/camera/camera/example/android/app/build.gradle | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 0606738a0a69..0b88fd10fb71 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -27,7 +27,7 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 30 + compileSdkVersion 29 defaultConfig { minSdkVersion 21 diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index d39a8da55cc8..7c6011b185fb 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -11,8 +11,6 @@ import android.content.IntentFilter; import android.content.res.Configuration; import android.hardware.SensorManager; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import android.provider.Settings; import android.view.Display; import android.view.OrientationEventListener; @@ -191,11 +189,6 @@ private int getDeviceDefaultOrientation() { @SuppressWarnings("deprecation") private Display getDisplay() { - if (VERSION.SDK_INT >= VERSION_CODES.R) { - return activity.getDisplay(); - } else { - return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay(); - } + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); } } diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index c5eeb246fe30..7d0e281b74e8 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' From fd412a30b0c8cc4a57783ce90b9b6480eabf02a2 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Wed, 13 Jan 2021 19:47:29 +0100 Subject: [PATCH 25/25] Revert "Rollback update to Android compileSdkVersion 30" This reverts commit 313595345ed7cdc79ea1e041c984faa55a9595ff. --- packages/camera/camera/android/build.gradle | 2 +- .../flutter/plugins/camera/DeviceOrientationManager.java | 9 ++++++++- packages/camera/camera/example/android/app/build.gradle | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 0b88fd10fb71..0606738a0a69 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -27,7 +27,7 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java index 7c6011b185fb..d39a8da55cc8 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -11,6 +11,8 @@ import android.content.IntentFilter; import android.content.res.Configuration; import android.hardware.SensorManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.provider.Settings; import android.view.Display; import android.view.OrientationEventListener; @@ -189,6 +191,11 @@ private int getDeviceDefaultOrientation() { @SuppressWarnings("deprecation") private Display getDisplay() { - return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + if (VERSION.SDK_INT >= VERSION_CODES.R) { + return activity.getDisplay(); + } else { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + } } } diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index 7d0e281b74e8..c5eeb246fe30 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 lintOptions { disable 'InvalidPackage'