diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 44df602b594..ad47eaa6b88 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.6.16 + +* Fixes incorrect camera preview rotation for landscape-oriented devices. +* Fixes regression where `onDeviceOrientationChanged` was not triggering with an initial orientation + after calling `createCameraWithSettings`. + ## 0.6.15+2 * Updates pigeon generated code to fix `ImplicitSamInstance` and `SyntheticAccessor` Kotlin lint diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java index fa391d2af49..bbc0b7d7ca7 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java @@ -49,9 +49,8 @@ Context getContext() { * the deliver orientation updates based on the UI orientation. */ public void start() { - if (broadcastReceiver != null) { - return; - } + stop(); + broadcastReceiver = new BroadcastReceiver() { @Override @@ -70,6 +69,8 @@ public void stop() { } getContext().unregisterReceiver(broadcastReceiver); broadcastReceiver = null; + + lastOrientation = null; } /** diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 82a584e102a..c38ca508a7e 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -13,7 +13,7 @@ import 'package:flutter/widgets.dart' show Texture, Widget, visibleForTesting; import 'package:stream_transform/stream_transform.dart'; import 'camerax_library.dart'; import 'camerax_proxy.dart'; -import 'rotated_preview.dart'; +import 'rotated_preview_delegate.dart'; /// The Android implementation of [CameraPlatform] that uses the CameraX library. class AndroidCameraCameraX extends CameraPlatform { @@ -256,6 +256,11 @@ class AndroidCameraCameraX extends CameraPlatform { /// The initial orientation of the device when the camera is created. late DeviceOrientation _initialDeviceOrientation; + /// The initial rotation of the Android default display when the camera is created. + /// + /// This is expressed in terms of one of the [Surface] rotation constant. + late int _initialDefaultDisplayRotation; + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { @@ -430,6 +435,8 @@ class AndroidCameraCameraX extends CameraPlatform { _initialDeviceOrientation = _deserializeDeviceOrientation( await deviceOrientationManager.getUiOrientation(), ); + _initialDefaultDisplayRotation = + await deviceOrientationManager.getDefaultDisplayRotation(); return flutterSurfaceTextureId; } @@ -917,31 +924,20 @@ class AndroidCameraCameraX extends CameraPlatform { ); } + final Stream deviceOrientationStream = + onDeviceOrientationChanged() + .map((DeviceOrientationChangedEvent e) => e.orientation); final Widget preview = Texture(textureId: cameraId); - if (_handlesCropAndRotation) { - return preview; - } - - final Stream deviceOrientationStream = - onDeviceOrientationChanged().map( - (DeviceOrientationChangedEvent e) => e.orientation, - ); - if (cameraIsFrontFacing) { - return RotatedPreview.frontFacingCamera( - _initialDeviceOrientation, - deviceOrientationStream, - sensorOrientationDegrees: sensorOrientationDegrees, - child: preview, - ); - } else { - return RotatedPreview.backFacingCamera( - _initialDeviceOrientation, - deviceOrientationStream, + return RotatedPreviewDelegate( + handlesCropAndRotation: _handlesCropAndRotation, + initialDeviceOrientation: _initialDeviceOrientation, + initialDefaultDisplayRotation: _initialDefaultDisplayRotation, + deviceOrientationStream: deviceOrientationStream, sensorOrientationDegrees: sensorOrientationDegrees, - child: preview, - ); - } + cameraIsFrontFacing: cameraIsFrontFacing, + deviceOrientationManager: deviceOrientationManager, + child: preview); } /// Captures an image and returns the file where it was saved. diff --git a/packages/camera/camera_android_camerax/lib/src/image_reader_rotated_preview.dart b/packages/camera/camera_android_camerax/lib/src/image_reader_rotated_preview.dart new file mode 100644 index 00000000000..de3a970fe8a --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/image_reader_rotated_preview.dart @@ -0,0 +1,164 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; + +import 'camerax_library.dart'; +import 'rotated_preview_utils.dart'; + +/// Widget that rotates the camera preview to be upright according to the +/// current user interface orientation for devices using the `ImageReader` +/// Impeller backend, which does not automatically handle the crop and +/// rotation of the camera preview correctly. +@internal +final class ImageReaderRotatedPreview extends StatefulWidget { + /// Creates [ImageReaderRotatedPreview] that will correct the preview + /// rotation assuming that the front camera is being used. + const ImageReaderRotatedPreview.frontFacingCamera( + this.initialDeviceOrientation, + this.initialDefaultDisplayRotation, + this.deviceOrientation, + this.sensorOrientationDegrees, + this.deviceOrientationManager, { + required this.child, + super.key, + }) : facingSign = 1; + + /// Creates [ImageReaderRotatedPreview] that will correct the preview + /// rotation assuming that the back camera is being used. + const ImageReaderRotatedPreview.backFacingCamera( + this.initialDeviceOrientation, + this.initialDefaultDisplayRotation, + this.deviceOrientation, + this.sensorOrientationDegrees, + this.deviceOrientationManager, { + required this.child, + super.key, + }) : facingSign = -1; + + /// The initial orientation of the device when the camera is created. + final DeviceOrientation initialDeviceOrientation; + + /// The initial rotation of the Android default display when the camera is created + /// in terms of a Surface rotation constant. + final int initialDefaultDisplayRotation; + + /// Stream of changes to the device orientation. + final Stream deviceOrientation; + + /// The orientation of the camera sensor in degrees. + final double sensorOrientationDegrees; + + /// The camera's device orientation manager. + /// + /// Instance required to check the current rotation of the default Android display. + final DeviceOrientationManager deviceOrientationManager; + + /// Value used to calculate the correct preview rotation. + /// + /// 1 if the camera is front facing; -1 if the camera is back facing. + final int facingSign; + + /// The camera preview [Widget] to rotate. + final Widget child; + + @override + State createState() => _ImageReaderRotatedPreviewState(); +} + +final class _ImageReaderRotatedPreviewState + extends State { + late DeviceOrientation deviceOrientation; + late Future defaultDisplayRotationDegrees; + late StreamSubscription deviceOrientationSubscription; + + Future _getCurrentDefaultDisplayRotationDegrees() async { + final int currentDefaultDisplayRotationQuarterTurns = + await widget.deviceOrientationManager.getDefaultDisplayRotation(); + return getQuarterTurnsFromSurfaceRotationConstant( + currentDefaultDisplayRotationQuarterTurns) * + 90; + } + + @override + void initState() { + deviceOrientation = widget.initialDeviceOrientation; + defaultDisplayRotationDegrees = Future.value( + getQuarterTurnsFromSurfaceRotationConstant( + widget.initialDefaultDisplayRotation) * + 90); + deviceOrientationSubscription = + widget.deviceOrientation.listen((DeviceOrientation event) { + // Ensure that we aren't updating the state if the widget is being destroyed. + if (!mounted) { + return; + } + + setState(() { + deviceOrientation = event; + defaultDisplayRotationDegrees = + _getCurrentDefaultDisplayRotationDegrees(); + }); + }); + super.initState(); + } + + double _computeRotationDegrees( + DeviceOrientation orientation, + int currentDefaultDisplayRotationDegrees, { + required double sensorOrientationDegrees, + required int sign, + }) { + // Rotate the camera preview according to + // https://developer.android.com/media/camera/camera2/camera-preview#orientation_calculation. + double rotationDegrees = (sensorOrientationDegrees - + currentDefaultDisplayRotationDegrees * sign + + 360) % + 360; + + // Then, subtract the rotation already applied in the CameraPreview widget + // (see camera/camera/lib/src/camera_preview.dart) that is not correct + // for this plugin. + final double extraRotationDegrees = + getPreAppliedQuarterTurnsRotationFromDeviceOrientation(orientation) * + 90; + rotationDegrees -= extraRotationDegrees; + + return rotationDegrees; + } + + @override + void dispose() { + deviceOrientationSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: defaultDisplayRotationDegrees, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + final int currentDefaultDisplayRotation = snapshot.data!; + final double rotationDegrees = _computeRotationDegrees( + deviceOrientation, + currentDefaultDisplayRotation, + sensorOrientationDegrees: widget.sensorOrientationDegrees, + sign: widget.facingSign, + ); + + return RotatedBox( + quarterTurns: rotationDegrees ~/ 90, + child: widget.child, + ); + } else { + return const SizedBox.shrink(); + } + }); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/rotated_preview_delegate.dart b/packages/camera/camera_android_camerax/lib/src/rotated_preview_delegate.dart new file mode 100644 index 00000000000..5e9aa3cb6a7 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/rotated_preview_delegate.dart @@ -0,0 +1,95 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; + +import 'camerax_library.g.dart'; +import 'image_reader_rotated_preview.dart'; +import 'surface_texture_rotated_preview.dart'; + +/// Widget that rotates the camera preview to be upright according to the +/// current user interface orientation based on whether or not the device +/// uses an Impeller backend that handles crop and rotation of Surfaces +/// correctly automatically. +@internal +final class RotatedPreviewDelegate extends StatelessWidget { + /// Creates [RotatedPreviewDelegate] that will build the correctly + /// rotated preview widget depending on whether or not the Impeller + /// backend handles crop and rotation automatically. + const RotatedPreviewDelegate( + {super.key, + required this.handlesCropAndRotation, + required this.initialDeviceOrientation, + required this.initialDefaultDisplayRotation, + required this.deviceOrientationStream, + required this.sensorOrientationDegrees, + required this.cameraIsFrontFacing, + required this.deviceOrientationManager, + required this.child}); + + /// Whether or not the Android surface producer automatically handles + /// correcting the rotation of camera previews for the device this plugin + /// runs on. + final bool handlesCropAndRotation; + + /// The initial orientation of the device when the camera is created. + final DeviceOrientation initialDeviceOrientation; + + /// The initial rotation of the Android default display when the camera is created, + /// in terms of a Surface rotation constant. + final int initialDefaultDisplayRotation; + + /// Stream of changes to the device orientation. + final Stream deviceOrientationStream; + + /// The orientation of the camera sensor in degrees. + final double sensorOrientationDegrees; + + /// Whether or not the camera is front facing. + final bool cameraIsFrontFacing; + + /// The camera's device orientation manager. + /// + /// Instance required to check the current rotation of the default Android display. + final DeviceOrientationManager deviceOrientationManager; + + /// The camera preview [Widget] to rotate. + final Widget child; + + @override + Widget build(BuildContext context) { + if (handlesCropAndRotation) { + return SurfaceTextureRotatedPreview( + initialDeviceOrientation, + initialDefaultDisplayRotation, + deviceOrientationStream, + deviceOrientationManager, + child: child); + } + + if (cameraIsFrontFacing) { + return ImageReaderRotatedPreview.frontFacingCamera( + initialDeviceOrientation, + initialDefaultDisplayRotation, + deviceOrientationStream, + sensorOrientationDegrees, + deviceOrientationManager, + child: child, + ); + } else { + return ImageReaderRotatedPreview.backFacingCamera( + initialDeviceOrientation, + initialDefaultDisplayRotation, + deviceOrientationStream, + sensorOrientationDegrees, + deviceOrientationManager, + child: child, + ); + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/rotated_preview_utils.dart b/packages/camera/camera_android_camerax/lib/src/rotated_preview_utils.dart new file mode 100644 index 00000000000..3bceb4e6717 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/rotated_preview_utils.dart @@ -0,0 +1,34 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; + +import 'camerax_library.dart' show Surface; + +/// Returns the number of counter-clockwise quarter turns represented by +/// [surfaceRotationConstant], a [Surface] constant representing a clockwise +/// rotation. +int getQuarterTurnsFromSurfaceRotationConstant(int surfaceRotationConstant) { + return switch (surfaceRotationConstant) { + Surface.rotation0 => 0, + Surface.rotation90 => 3, + Surface.rotation180 => 2, + Surface.rotation270 => 1, + int() => throw ArgumentError( + '$surfaceRotationConstant is an unknown Surface rotation constant, so counter-clockwise quarter turns cannot be determined.'), + }; +} + +/// Returns the clockwise quarter turns applied by the CameraPreview widget +/// based on [orientation], the current device orientation (see +/// camera/camera/lib/src/camera_preview.dart). +int getPreAppliedQuarterTurnsRotationFromDeviceOrientation( + DeviceOrientation orientation) { + return switch (orientation) { + DeviceOrientation.portraitUp => 0, + DeviceOrientation.landscapeRight => 1, + DeviceOrientation.portraitDown => 2, + DeviceOrientation.landscapeLeft => 3, + }; +} diff --git a/packages/camera/camera_android_camerax/lib/src/surface_texture_rotated_preview.dart b/packages/camera/camera_android_camerax/lib/src/surface_texture_rotated_preview.dart new file mode 100644 index 00000000000..976fd0fa575 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/surface_texture_rotated_preview.dart @@ -0,0 +1,117 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; + +import 'camerax_library.dart' show DeviceOrientationManager; +import 'rotated_preview_utils.dart'; + +/// Widget that rotates the camera preview to be upright according to the +/// current user interface orientation when the preview is backed by a +/// native Android `SurfaceTexture`, which does handle the crop and rotation +/// of the camera preview automatically. +@internal +final class SurfaceTextureRotatedPreview extends StatefulWidget { + /// Creates [SurfaceTextureRotatedPreview] that will rotate camera preview + /// according to the rotation of the Android default display. + const SurfaceTextureRotatedPreview( + this.initialDeviceOrientation, + this.initialDefaultDisplayRotation, + this.deviceOrientationStream, + this.deviceOrientationManager, + {required this.child, + super.key}); + + /// The initial orientation of the device when the camera is created. + final DeviceOrientation initialDeviceOrientation; + + /// The initial rotation of the Android default display when the camera is created + /// in terms of a Surface rotation constant. + final int initialDefaultDisplayRotation; + + /// Stream of changes to the device orientation. + final Stream deviceOrientationStream; + + /// The camera's device orientation manager. + /// + /// Instance required to check the current rotation of the default Android display. + final DeviceOrientationManager deviceOrientationManager; + + /// The camera preview [Widget] to rotate. + final Widget child; + + @override + State createState() => _SurfaceTextureRotatedPreviewState(); +} + +final class _SurfaceTextureRotatedPreviewState + extends State { + late StreamSubscription deviceOrientationSubscription; + late int preappliedRotationQuarterTurns; + late Future defaultDisplayRotationQuarterTurns; + + Future _getCurrentDefaultDisplayRotationQuarterTurns() async { + final int currentDefaultDisplayRotationQuarterTurns = + await widget.deviceOrientationManager.getDefaultDisplayRotation(); + return getQuarterTurnsFromSurfaceRotationConstant( + currentDefaultDisplayRotationQuarterTurns); + } + + @override + void initState() { + preappliedRotationQuarterTurns = + getPreAppliedQuarterTurnsRotationFromDeviceOrientation( + widget.initialDeviceOrientation); + defaultDisplayRotationQuarterTurns = Future.value( + getQuarterTurnsFromSurfaceRotationConstant( + widget.initialDefaultDisplayRotation)); + deviceOrientationSubscription = + widget.deviceOrientationStream.listen((DeviceOrientation event) { + // Ensure that we aren't updating the state if the widget is being destroyed. + if (!mounted) { + return; + } + + setState(() { + preappliedRotationQuarterTurns = + getPreAppliedQuarterTurnsRotationFromDeviceOrientation(event); + defaultDisplayRotationQuarterTurns = + _getCurrentDefaultDisplayRotationQuarterTurns(); + }); + }); + super.initState(); + } + + @override + void dispose() { + deviceOrientationSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: defaultDisplayRotationQuarterTurns, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + // Rotated preview according to current default display rotation, + // but subtract out rotation applied by the CameraPreview widget + // (see camera/camera/lib/src/camera_preview.dart) that is not + // correct for this plugin. + final int currentDefaultDisplayRotation = snapshot.data!; + final int rotationCorrection = + currentDefaultDisplayRotation - preappliedRotationQuarterTurns; + + return RotatedBox( + quarterTurns: rotationCorrection, child: widget.child); + } else { + return const SizedBox.shrink(); + } + }); + } +} diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 68f08917790..8eb65f428cf 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.6.15+2 +version: 0.6.16 environment: sdk: ^3.6.0 diff --git a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart index fc651d779ce..13c4cda98fd 100644 --- a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart +++ b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart @@ -14,10 +14,30 @@ import 'package:mockito/mockito.dart'; import 'android_camera_camerax_test.mocks.dart'; // Constants to map clockwise degree rotations to quarter turns: +const int _0DegreesClockwise = 0; const int _90DegreesClockwise = 1; +const int _180DegreesClockwise = 2; const int _270DegreesClockwise = 3; +// Serialize DeviceOrientations to Strings. +String _serializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.portraitUp: + return 'PORTRAIT_UP'; + case DeviceOrientation.landscapeLeft: + return 'LANDSCAPE_LEFT'; + case DeviceOrientation.portraitDown: + return 'PORTRAIT_DOWN'; + case DeviceOrientation.landscapeRight: + return 'LANDSCAPE_RIGHT'; + } +} + void main() { + tearDownAll(() { + AndroidCameraCameraX.deviceOrientationChangedStreamController.close(); + }); + /// Sets up mock CameraSelector and mock ProcessCameraProvider used to /// select test camera when `availableCameras` is called. /// @@ -60,8 +80,12 @@ void main() { } /// Returns CameraXProxy used to mock all calls to native Android in - /// the `availableCameras` and `createCameraWithSettings` methods. - CameraXProxy getProxyForCreatingTestCamera({ + /// the `availableCameras` and `createCameraWithSettings` methods, with + /// a DeviceORientationManager specified. + /// + /// Useful for tests that need a reference to a DeviceOrientationManager. + CameraXProxy getProxyForCreatingTestCameraWithDeviceOrientationManager( + DeviceOrientationManager deviceOrientationManager, { required MockProcessCameraProvider mockProcessCameraProvider, required CameraSelector Function({ LensFacing? requireLensFacing, @@ -71,7 +95,6 @@ void main() { PigeonInstanceManager? pigeon_instanceManager, }) createCameraSelector, required bool handlesCropAndRotation, - required Future Function() getUiOrientation, }) => CameraXProxy( getInstanceProcessCameraProvider: ({ @@ -228,13 +251,8 @@ void main() { BinaryMessenger? pigeon_binaryMessenger, // ignore: non_constant_identifier_names PigeonInstanceManager? pigeon_instanceManager, - }) { - final MockDeviceOrientationManager manager = - MockDeviceOrientationManager(); - when(manager.getUiOrientation()) - .thenAnswer((_) => getUiOrientation()); - return manager; - }, // 3 is a random Flutter SurfaceTexture ID for testing + }) => + deviceOrientationManager, newAspectRatioStrategy: ({ required AspectRatio preferredAspectRatio, required AspectRatioStrategyFallbackRule fallbackRule, @@ -264,6 +282,38 @@ void main() { }, ); + /// Returns CameraXProxy used to mock all calls to native Android in + /// the `availableCameras` and `createCameraWithSettings` methods, with + /// functions `getUiOrientation` and `getDefaultDisplayRotation` specified + /// to create a mock DeviceOrientationManager. + /// + /// Useful for tests that do not need a reference to a DeviceOrientationManager. + CameraXProxy getProxyForCreatingTestCamera({ + required MockProcessCameraProvider mockProcessCameraProvider, + required CameraSelector Function({ + LensFacing? requireLensFacing, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) createCameraSelector, + required bool handlesCropAndRotation, + required Future Function() getUiOrientation, + required Future Function() getDefaultDisplayRotation, + }) { + final MockDeviceOrientationManager deviceOrientationManager = + MockDeviceOrientationManager(); + when(deviceOrientationManager.getUiOrientation()) + .thenAnswer((_) => getUiOrientation()); + when(deviceOrientationManager.getDefaultDisplayRotation()) + .thenAnswer((_) => getDefaultDisplayRotation()); + return getProxyForCreatingTestCameraWithDeviceOrientationManager( + deviceOrientationManager, + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: createCameraSelector, + handlesCropAndRotation: handlesCropAndRotation); + } + /// Returns function that a CameraXProxy can use to select the front camera. MockCameraSelector Function({ LensFacing? requireLensFacing, @@ -325,197 +375,644 @@ void main() { ) => 'Expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated $actualQuarterTurns quarter turns.'; - testWidgets( - 'when handlesCropAndRotation is true, the preview is an unrotated Texture', - (WidgetTester tester) async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); - const int cameraId = 537; - const MediaSettings testMediaSettings = - MediaSettings(); // media settings irrelevant for test - - // Set up test camera (specifics irrelevant for this test) and - // tell camera that handlesCropAndRotation is true. + group('when handlesCropAndRotation is true', () { + // Test that preview rotation responds to initial default display rotation: + group('initial device orientation is landscapeRight,', () { final MockCameraSelector mockCameraSelector = MockCameraSelector(); - final MockProcessCameraProvider mockProcessCameraProvider = - setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( - mockCameraSelector: mockCameraSelector, - sensorRotationDegrees: /* irrelevant for test */ 90, - ); - camera.proxy = getProxyForCreatingTestCamera( - mockProcessCameraProvider: mockProcessCameraProvider, - createCameraSelector: ({ - LensFacing? requireLensFacing, + late AndroidCameraCameraX camera; + late int cameraId; + late DeviceOrientation testInitialDeviceOrientation; + late MockProcessCameraProvider mockProcessCameraProvider; + late MockCameraSelector Function( // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, + {BinaryMessenger? pigeon_binaryMessenger, // ignore: non_constant_identifier_names PigeonInstanceManager? pigeon_instanceManager, - }) => - mockCameraSelector, - handlesCropAndRotation: true, - /* irrelevant for test */ getUiOrientation: () async => - _serializeDeviceOrientation(DeviceOrientation.landscapeLeft), - ); + LensFacing? requireLensFacing}) fakeCreateCameraSelector; + late MediaSettings testMediaSettings; - // Get and create test camera. - final List availableCameras = - await camera.availableCameras(); - expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + setUp(() { + camera = AndroidCameraCameraX(); + cameraId = 7; - // Put camera preview in widget tree. - await tester.pumpWidget(camera.buildPreview(cameraId)); + // Set test camera initial device orientation for test. + testInitialDeviceOrientation = DeviceOrientation.landscapeRight; - // Verify Texture was built. - final Texture texture = tester.widget(find.byType(Texture)); - expect(texture.textureId, cameraId); + // Set up test camera and fake camera selector (specifics irrelevant for this test). + mockProcessCameraProvider = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockCameraSelector, + sensorRotationDegrees: /* irrelevant for test */ 90); + fakeCreateCameraSelector = + createCameraSelectorForBackCamera(mockCameraSelector); - // Verify RotatedBox was not built and thus, the Texture is not rotated. - expect( - () => tester.widget(find.byType(RotatedBox)), - throwsStateError, - ); - }, - ); + // Media settings to create camera; irrelevant for test. + testMediaSettings = const MediaSettings(); + }); + + testWidgets( + 'initial default display rotation is 0 degrees clockwise, then the preview Texture is rotation 270 degrees clockwise', + (WidgetTester tester) async { + // Mock calls to CameraXProxy. Most importantly, tell camera that handlesCropAndRotation is true, set initial device + // orientation to landscape right, and set initial default display rotation to 0 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: fakeCreateCameraSelector, + handlesCropAndRotation: true, + getUiOrientation: () async => + _serializeDeviceOrientation(testInitialDeviceOrientation), + getDefaultDisplayRotation: () => + Future.value(Surface.rotation0)); + + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + + // Verify Texture is rotated by 0 - 90 = -90 degrees clockwise = 270 degrees clockwise. + const int expectedQuarterTurns = _270DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + + testWidgets( + 'initial default display rotation is 90 degrees clockwise, then the preview Texture is rotation 180 degrees clockwise', + (WidgetTester tester) async { + // Mock calls to CameraXProxy. Most importantly, tell camera that handlesCropAndRotation is true, set initial device + // orientation to landscape right, and set initial default display rotation to 90 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: fakeCreateCameraSelector, + handlesCropAndRotation: true, + getUiOrientation: () async => + _serializeDeviceOrientation(testInitialDeviceOrientation), + getDefaultDisplayRotation: () => + Future.value(Surface.rotation90)); + + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + + // Verify Texture is rotated by 270 - 90 = 180 degrees clockwise. + const int expectedQuarterTurns = _180DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + testWidgets( + 'initial default display rotation is 180 degrees clockwise, then the preview Texture is rotation 90 degrees clockwise', + (WidgetTester tester) async { + // Mock calls to CameraXProxy. Most importantly, tell camera that handlesCropAndRotation is true, set initial device + // orientation to landscape right, and set initial default display rotation to 180 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: fakeCreateCameraSelector, + handlesCropAndRotation: true, + getUiOrientation: () async => + _serializeDeviceOrientation(testInitialDeviceOrientation), + getDefaultDisplayRotation: () => + Future.value(Surface.rotation180)); + + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + + // Verify Texture is rotated by 180 - 90 = 90 degrees clockwise. + const int expectedQuarterTurns = _90DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + testWidgets( + 'initial default display rotation is 270 degrees clockwise, then the preview Texture is rotation 0 degrees clockwise', + (WidgetTester tester) async { + // Mock calls to CameraXProxy. Most importantly, tell camera that handlesCropAndRotation is true, set initial device + // orientation to landscape right, and set initial default display rotation to 270 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: fakeCreateCameraSelector, + handlesCropAndRotation: true, + getUiOrientation: () async => + _serializeDeviceOrientation(testInitialDeviceOrientation), + getDefaultDisplayRotation: () => + Future.value(Surface.rotation270)); + + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + + // Verify Texture is rotated by 90 - 90 = 0 degrees. + const int expectedQuarterTurns = _0DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + }); - group('when handlesCropAndRotation is false,', () { // Test that preview rotation responds to initial device orientation: - group('sensor orientation degrees is 270, camera is front facing,', () { + group('initial default display rotation is 90,', () { + final MockCameraSelector mockCameraSelector = MockCameraSelector(); late AndroidCameraCameraX camera; late int cameraId; - late MockCameraSelector mockFrontCameraSelector; - late MockCameraSelector Function({ - LensFacing? requireLensFacing, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) proxyCreateCameraSelectorForFrontCamera; - late MockProcessCameraProvider mockProcessCameraProviderForFrontCamera; + late int testInitialDefaultDisplayRotation; + late MockProcessCameraProvider mockProcessCameraProvider; + late MockCameraSelector Function( + // ignore: non_constant_identifier_names + {BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + LensFacing? requireLensFacing}) fakeCreateCameraSelector; late MediaSettings testMediaSettings; setUp(() { camera = AndroidCameraCameraX(); - cameraId = 27; + cameraId = 7; - // Create and set up mock CameraSelector and mock ProcessCameraProvider for test front camera - // with sensor orientation degrees 270. - mockFrontCameraSelector = MockCameraSelector(); - proxyCreateCameraSelectorForFrontCamera = - createCameraSelectorForFrontCamera(mockFrontCameraSelector); - mockProcessCameraProviderForFrontCamera = + // Set test camera initial default display rotation for test. + testInitialDefaultDisplayRotation = Surface.rotation90; + + // Set up test camera (specifics irrelevant for this test). + mockProcessCameraProvider = setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( - mockCameraSelector: mockFrontCameraSelector, - sensorRotationDegrees: 270, - ); + mockCameraSelector: mockCameraSelector, + sensorRotationDegrees: /* irrelevant for test */ 90); + fakeCreateCameraSelector = + createCameraSelectorForBackCamera(mockCameraSelector); // Media settings to create camera; irrelevant for test. testMediaSettings = const MediaSettings(); }); testWidgets( - 'initial device orientation fixed to DeviceOrientation.portraitUp, then the preview Texture is rotated 270 degrees clockwise', - (WidgetTester tester) async { - // Set up test to use front camera, tell camera that handlesCropAndRotation is false, - // set camera initial device orientation to portrait up. - camera.proxy = getProxyForCreatingTestCamera( - mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, - createCameraSelector: proxyCreateCameraSelectorForFrontCamera, - handlesCropAndRotation: false, + 'initial device orientation is portraitUp, then the preview Texture is rotation 270 degrees clockwise', + (WidgetTester tester) async { + // Mock calls to CameraXProxy. Most importantly, tell camera that handlesCropAndRotation is true, set initial device + // orientation to portrait up, and set initial default display rotation to 90 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: fakeCreateCameraSelector, + handlesCropAndRotation: true, getUiOrientation: () async => _serializeDeviceOrientation(DeviceOrientation.portraitUp), - ); + getDefaultDisplayRotation: () => + Future.value(testInitialDefaultDisplayRotation)); - // Get and create test front camera. - final List availableCameras = - await camera.availableCameras(); - expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); - // Put camera preview in widget tree. - await tester.pumpWidget(camera.buildPreview(cameraId)); + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); - // Verify Texture is rotated by ((270 - 0 * 1 + 360) % 360) - 0 = 270 degrees. - const int expectedQuarterTurns = _270DegreesClockwise; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); - expect( - rotatedBox.quarterTurns, - expectedQuarterTurns, - reason: getExpectedRotationTestFailureReason( - expectedQuarterTurns, - rotatedBox.quarterTurns, - ), - ); - }, - ); + // Verify Texture was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + + // Verify Texture is rotated by 270 - 0 = 270 degrees clockwise. + const int expectedQuarterTurns = _270DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); testWidgets( - 'initial device orientation fixed to DeviceOrientation.landscapeRight, then the preview Texture is rotated 90 degrees clockwise', - (WidgetTester tester) async { - // Set up test to use front camera, tell camera that handlesCropAndRotation is false, - // set camera initial device orientation to landscape right. - camera.proxy = getProxyForCreatingTestCamera( - mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, - createCameraSelector: proxyCreateCameraSelectorForFrontCamera, - handlesCropAndRotation: false, - getUiOrientation: () async => _serializeDeviceOrientation( - DeviceOrientation.landscapeRight, - ), - ); + 'initial device orientation is landscapeLeft, then the preview Texture is rotation 0 degrees clockwise', + (WidgetTester tester) async { + // Mock calls to CameraXProxy. Most importantly, tell camera that handlesCropAndRotation is true, set initial device + // orientation to landscape left, and set initial default display rotation to 90 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: fakeCreateCameraSelector, + handlesCropAndRotation: true, + getUiOrientation: () async => + _serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + getDefaultDisplayRotation: () => + Future.value(testInitialDefaultDisplayRotation)); - // Get and create test front camera. - final List availableCameras = - await camera.availableCameras(); - expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); - // Put camera preview in widget tree. - await tester.pumpWidget(camera.buildPreview(cameraId)); + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); - // Verify Texture is rotated by ((90 - 270 * 1 + 360) % 360) - 90 = 90 degrees. - const int expectedQuarterTurns = _90DegreesClockwise; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); - expect( - rotatedBox.quarterTurns, - expectedQuarterTurns, - reason: getExpectedRotationTestFailureReason( - expectedQuarterTurns, - rotatedBox.quarterTurns, - ), - ); - }, - ); + // Verify Texture was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + // Verify Texture is rotated by 270 - 270 = 0 degrees. + const int expectedQuarterTurns = _0DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); testWidgets( - 'initial device orientation fixed to DeviceOrientation.portraitDown, then the preview Texture is rotated 270 degrees clockwise', - (WidgetTester tester) async { - // Set up test to use front camera, tell camera that handlesCropAndRotation is false, - // set camera initial device orientation to portrait down. - camera.proxy = getProxyForCreatingTestCamera( - mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, - createCameraSelector: proxyCreateCameraSelectorForFrontCamera, - handlesCropAndRotation: false, + 'initial device orientation is portraitDown, then the preview Texture is rotation 90 degrees clockwise', + (WidgetTester tester) async { + // Mock calls to CameraXProxy. Most importantly, tell camera that handlesCropAndRotation is true, set initial device + // orientation to portrait down, and set initial default display rotation to 90 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: fakeCreateCameraSelector, + handlesCropAndRotation: true, getUiOrientation: () async => _serializeDeviceOrientation(DeviceOrientation.portraitDown), - ); + getDefaultDisplayRotation: () => + Future.value(testInitialDefaultDisplayRotation)); - // Get and create test front camera. + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + + // Verify Texture is rotated by 270 - 180 = 90 degrees clockwise. + const int expectedQuarterTurns = _90DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + testWidgets( + 'initial device orientation is landscapeRight, then the preview Texture is rotation 180 degrees clockwise', + (WidgetTester tester) async { + // Mock calls to CameraXProxy. Most importantly, tell camera that handlesCropAndRotation is true, set initial device + // orientation to landscape right, and set initial default display rotation to 90 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: fakeCreateCameraSelector, + handlesCropAndRotation: true, + getUiOrientation: () async => + _serializeDeviceOrientation(DeviceOrientation.landscapeRight), + getDefaultDisplayRotation: () => + Future.value(testInitialDefaultDisplayRotation)); + + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + + // Verify Texture is rotated by 270 - 90 = 180 degrees clockwise. + const int expectedQuarterTurns = _180DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + }); + + // Test that preview rotation responds to change in default display rotation: + testWidgets( + 'device orientation is portraitDown, then the preview Texture rotates correctly as the default display rotation changes', + (WidgetTester tester) async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 11; + const DeviceOrientation testDeviceOrientation = + DeviceOrientation.portraitDown; + + // Create and set up mock CameraSelector, mock ProcessCameraProvider, and media settings for test front camera. + // These settings do not matter for this test. + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockCameraSelector Function( + // ignore: non_constant_identifier_names + {BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + LensFacing? requireLensFacing}) + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + final MockProcessCameraProvider mockProcessCameraProviderForFrontCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockFrontCameraSelector, + sensorRotationDegrees: 270); + const MediaSettings testMediaSettings = MediaSettings(); + + // Tell camera that handlesCropAndRotation is true, set camera initial device orientation + // to portrait down, set initial default display rotation to 0 degrees clockwise. + final MockDeviceOrientationManager mockDeviceOrientationManager = + MockDeviceOrientationManager(); + when(mockDeviceOrientationManager.getUiOrientation()).thenAnswer((_) => + Future.value( + _serializeDeviceOrientation(testDeviceOrientation))); + when(mockDeviceOrientationManager.getDefaultDisplayRotation()) + .thenAnswer((_) => Future.value(Surface.rotation0)); + + camera.proxy = getProxyForCreatingTestCameraWithDeviceOrientationManager( + mockDeviceOrientationManager, + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + handlesCropAndRotation: true); + + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Calculated according to: counterClockwiseCurrentDefaultDisplayRotation - cameraPreviewPreAppliedRotation, + // where the cameraPreviewPreAppliedRotation is the clockwise rotation applied by the CameraPreview widget + // according to the current device orientation (fixed to portraitDown for this test, so it is 180). + final Map expectedRotationPerDefaultDisplayRotation = + { + Surface.rotation0: _180DegreesClockwise, + Surface.rotation90: _90DegreesClockwise, + Surface.rotation180: _0DegreesClockwise, + Surface.rotation270: _270DegreesClockwise, + }; + + // Put camera preview in widget tree. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + for (final int currentDefaultDisplayRotation + in expectedRotationPerDefaultDisplayRotation.keys) { + // Modify CameraXProxy to return the default display rotation we want to test. + when(mockDeviceOrientationManager.getDefaultDisplayRotation()) + .thenAnswer( + (_) => Future.value(currentDefaultDisplayRotation)); + + const DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(testDeviceOrientation); + AndroidCameraCameraX.deviceOrientationChangedStreamController + .add(testEvent); + + await tester.pumpAndSettle(); + + // Verify Texture is rotated by expected clockwise degrees. + final int expectedQuarterTurns = + expectedRotationPerDefaultDisplayRotation[ + currentDefaultDisplayRotation]!; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns < 0 + ? rotatedBox.quarterTurns + 4 + : rotatedBox.quarterTurns; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: + 'When the default display rotation is $currentDefaultDisplayRotation, expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated ${rotatedBox.quarterTurns} quarter turns.'); + } + }); + + // Test that preview rotation responds to change in device orientation: + testWidgets( + 'initial default display rotation is 270 degrees clockwise, then the preview Texture rotates correctly as the device orientation changes', + (WidgetTester tester) async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 11; + const int testInitialDefaultDisplayRotation = Surface.rotation270; + + // Create and set up mock CameraSelector, mock ProcessCameraProvider, and media settings for test front camera. + // These settings do not matter for this test. + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockCameraSelector Function( + // ignore: non_constant_identifier_names + {BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + LensFacing? requireLensFacing}) + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + final MockProcessCameraProvider mockProcessCameraProviderForFrontCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockFrontCameraSelector, + sensorRotationDegrees: 270); + const MediaSettings testMediaSettings = MediaSettings(); + + // Tell camera that handlesCropAndRotation is true, set camera initial device orientation + // to portrait up, set initial default display rotation to 270 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + handlesCropAndRotation: true, + getUiOrientation: /* initial device orientation is irrelevant */ + () async => + _serializeDeviceOrientation(DeviceOrientation.portraitUp), + getDefaultDisplayRotation: () => + Future.value(testInitialDefaultDisplayRotation)); + + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Calculated according to: counterClockwiseCurrentDefaultDisplayRotation - cameraPreviewPreAppliedRotation, + // where the cameraPreviewPreAppliedRotation is the clockwise rotation applied by the CameraPreview widget + // according to the current device orientation. counterClockwiseCurrentDefaultDisplayRotation is fixed to 90 for + // this test (the counter-clockwise rotation of the clockwise 270 degree default display rotation). + final Map expectedRotationPerDeviceOrientation = + { + DeviceOrientation.portraitUp: _90DegreesClockwise, + DeviceOrientation.landscapeRight: _0DegreesClockwise, + DeviceOrientation.portraitDown: _270DegreesClockwise, + DeviceOrientation.landscapeLeft: _180DegreesClockwise, + }; + + // Put camera preview in widget tree. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + for (final DeviceOrientation currentDeviceOrientation + in expectedRotationPerDeviceOrientation.keys) { + final DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(currentDeviceOrientation); + AndroidCameraCameraX.deviceOrientationChangedStreamController + .add(testEvent); + + await tester.pumpAndSettle(); + + // Verify Texture is rotated by expected clockwise degrees. + final int expectedQuarterTurns = + expectedRotationPerDeviceOrientation[currentDeviceOrientation]!; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns < 0 + ? rotatedBox.quarterTurns + 4 + : rotatedBox.quarterTurns; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: + 'When the device orientation is $currentDeviceOrientation, expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated ${rotatedBox.quarterTurns} quarter turns.'); + } + }); + }); + + group('when handlesCropAndRotation is false,', () { + // Test that preview rotation responds to initial device orientation: + group( + 'sensor orientation degrees is 270, camera is front facing, initial default display rotation is 0 degrees clockwise', + () { + late AndroidCameraCameraX camera; + late int cameraId; + late MockCameraSelector mockFrontCameraSelector; + late MockCameraSelector Function({ + LensFacing? requireLensFacing, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) proxyCreateCameraSelectorForFrontCamera; + late MockProcessCameraProvider mockProcessCameraProviderForFrontCamera; + late Future Function() proxyGetDefaultDisplayRotation; + late MediaSettings testMediaSettings; + + setUp(() { + camera = AndroidCameraCameraX(); + cameraId = 27; + + // Create and set up mock CameraSelector and mock ProcessCameraProvider for test front camera + // with sensor orientation degrees 270. Also, set up function to mock initial default display + // of 0. + mockFrontCameraSelector = MockCameraSelector(); + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + mockProcessCameraProviderForFrontCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockFrontCameraSelector, + sensorRotationDegrees: 270, + ); + proxyGetDefaultDisplayRotation = + () => Future.value(Surface.rotation0); + + // Media settings to create camera; irrelevant for test. + testMediaSettings = const MediaSettings(); + }); + + testWidgets( + 'initial device orientation fixed to DeviceOrientation.portraitUp, then the preview Texture is rotated 270 degrees clockwise', + (WidgetTester tester) async { + // Set up test to use front camera, tell camera that handlesCropAndRotation is false, + // set camera initial device orientation to portrait up. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: proxyGetDefaultDisplayRotation, + handlesCropAndRotation: false, + getUiOrientation: () async => + _serializeDeviceOrientation(DeviceOrientation.portraitUp), + ); + + // Get and create test front camera. final List availableCameras = await camera.availableCameras(); expect(availableCameras.length, 1); @@ -524,19 +1021,20 @@ void main() { testMediaSettings, ); - // Put camera preview in widget tree. + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); - // Verify Texture is rotated by ((270 - 180 * 1 + 360) % 360) - 180 = -90 degrees clockwise = 90 degrees counterclockwise = 270 degrees. + // Verify Texture is rotated by ((270 - 0 * 1 + 360) % 360) - 0 = 270 degrees. const int expectedQuarterTurns = _270DegreesClockwise; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.child, isA()); expect((rotatedBox.child! as Texture).textureId, cameraId); expect( - clockwiseQuarterTurns, + rotatedBox.quarterTurns, expectedQuarterTurns, reason: getExpectedRotationTestFailureReason( expectedQuarterTurns, @@ -547,16 +1045,17 @@ void main() { ); testWidgets( - 'initial device orientation fixed to DeviceOrientation.landscapeLeft, then the preview Texture is rotated 90 degrees clockwise', + 'initial device orientation fixed to DeviceOrientation.landscapeRight, then the preview Texture is rotated 180 degrees clockwise', (WidgetTester tester) async { // Set up test to use front camera, tell camera that handlesCropAndRotation is false, - // set camera initial device orientation to landscape left. + // set camera initial device orientation to landscape right. camera.proxy = getProxyForCreatingTestCamera( mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: proxyGetDefaultDisplayRotation, handlesCropAndRotation: false, getUiOrientation: () async => _serializeDeviceOrientation( - DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, ), ); @@ -569,19 +1068,19 @@ void main() { testMediaSettings, ); - // Put camera preview in widget tree. + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); - // Verify Texture is rotated by ((270 - 270 * 1 + 360) % 360) - 270 = -270 degrees clockwise = 270 degrees counterclockwise = 90 degrees. - const int expectedQuarterTurns = _90DegreesClockwise; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + // Verify Texture is rotated by ((270 - 0 * 1 + 360) % 360) - 90 = 180 degrees. + const int expectedQuarterTurns = _180DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); expect(rotatedBox.child, isA()); expect((rotatedBox.child! as Texture).textureId, cameraId); expect( - clockwiseQuarterTurns, + rotatedBox.quarterTurns, expectedQuarterTurns, reason: getExpectedRotationTestFailureReason( expectedQuarterTurns, @@ -590,44 +1089,65 @@ void main() { ); }, ); - }); - testWidgets( - 'sensor orientation degrees is 90, camera is front facing, then the preview Texture rotates correctly as the device orientation rotates', - (WidgetTester tester) async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); - const int cameraId = 3372; + testWidgets( + 'initial device orientation fixed to DeviceOrientation.portraitDown, then the preview Texture is rotated 90 degrees clockwise', + (WidgetTester tester) async { + // Set up test to use front camera, tell camera that handlesCropAndRotation is false, + // set camera initial device orientation to portrait down. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: proxyGetDefaultDisplayRotation, + handlesCropAndRotation: false, + getUiOrientation: () async => + _serializeDeviceOrientation(DeviceOrientation.portraitDown), + ); - // Create and set up mock CameraSelector and mock ProcessCameraProvider for test front camera - // with sensor orientation degrees 90. - final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); - final MockCameraSelector Function({ - LensFacing? requireLensFacing, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) proxyCreateCameraSelectorForFrontCamera = - createCameraSelectorForFrontCamera(mockFrontCameraSelector); - final MockProcessCameraProvider - mockProcessCameraProviderForFrontCamera = - setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( - mockCameraSelector: mockFrontCameraSelector, - sensorRotationDegrees: 90, - ); + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); - // Media settings to create camera; irrelevant for test. - const MediaSettings testMediaSettings = MediaSettings(); + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((270 - 0 * 1 + 360) % 360) - 180 = 90 degrees clockwise. + const int expectedQuarterTurns = _90DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect( + rotatedBox.quarterTurns, + expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, + rotatedBox.quarterTurns, + ), + ); + }, + ); - // Set up test to use front camera and tell camera that handlesCropAndRotation is false, + testWidgets( + 'initial device orientation fixed to DeviceOrientation.landscapeLeft, then the preview Texture is rotated 0 degrees clockwise', + (WidgetTester tester) async { + // Set up test to use front camera, tell camera that handlesCropAndRotation is false, // set camera initial device orientation to landscape left. camera.proxy = getProxyForCreatingTestCamera( mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: proxyGetDefaultDisplayRotation, handlesCropAndRotation: false, - getUiOrientation: /* initial device orientation irrelevant for test */ - () async => - _serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + getUiOrientation: () async => _serializeDeviceOrientation( + DeviceOrientation.landscapeLeft, + ), ); // Get and create test front camera. @@ -639,349 +1159,669 @@ void main() { testMediaSettings, ); - // Calculated according to: - // ((90 - currentDeviceOrientation * 1 + 360) % 360) - currentDeviceOrientation. - final Map expectedRotationPerDeviceOrientation = - { - DeviceOrientation.portraitUp: _90DegreesClockwise, - DeviceOrientation.landscapeRight: _270DegreesClockwise, - DeviceOrientation.portraitDown: _90DegreesClockwise, - DeviceOrientation.landscapeLeft: _270DegreesClockwise, - }; - - // Put camera preview in widget tree. + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((270 - 0 * 1 + 360) % 360) - 270 = 0 degrees clockwise. + const int expectedQuarterTurns = _0DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + }); - for (final DeviceOrientation currentDeviceOrientation - in expectedRotationPerDeviceOrientation.keys) { - final DeviceOrientationChangedEvent testEvent = - DeviceOrientationChangedEvent(currentDeviceOrientation); - AndroidCameraCameraX.deviceOrientationChangedStreamController.add( - testEvent, - ); + // Test that preview rotation responds to initial default display rotation: + group( + 'initial device orientation fixed to DeviceOrientation.landscapeLeft, sensor orientation degrees is 270, camera is front facing,', + () { + late AndroidCameraCameraX camera; + late int cameraId; + late MockCameraSelector mockFrontCameraSelector; + late MockProcessCameraProvider mockProcessCameraProviderForFrontCamera; + late MockCameraSelector Function( + // ignore: non_constant_identifier_names + {BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + LensFacing? requireLensFacing}) + proxyCreateCameraSelectorForFrontCamera; + late Future Function() proxyGetUiOrientation; + late MediaSettings testMediaSettings; - await tester.pumpAndSettle(); + setUp(() { + camera = AndroidCameraCameraX(); + cameraId = 48; - // Verify Texture is rotated by expected clockwise degrees. - final int expectedQuarterTurns = - expectedRotationPerDeviceOrientation[currentDeviceOrientation]!; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - final int clockwiseQuarterTurns = rotatedBox.quarterTurns < 0 - ? rotatedBox.quarterTurns + 4 - : rotatedBox.quarterTurns; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); - expect( - clockwiseQuarterTurns, - expectedQuarterTurns, + // Create and set up mock CameraSelector and mock ProcessCameraProvider for test front camera + // with sensor orientation degrees 270. Also, set up function to mock initial default display + // of 0. + mockFrontCameraSelector = MockCameraSelector(); + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + mockProcessCameraProviderForFrontCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockFrontCameraSelector, + sensorRotationDegrees: 270); + proxyGetUiOrientation = () async => + _serializeDeviceOrientation(DeviceOrientation.landscapeLeft); + + // Media settings to create camera; irrelevant for test. + testMediaSettings = const MediaSettings(); + }); + + testWidgets( + 'initial default display rotation is 0 degrees, then the preview Texture is rotated 0 degrees clockwise', + (WidgetTester tester) async { + // Set up test to use front camera, tell camera that handlesCropAndRotation is false, + // set camera initial default display rotation to 0 degrees. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: () => + Future.value(Surface.rotation0), + handlesCropAndRotation: false, + getUiOrientation: proxyGetUiOrientation); + + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((270 - 0 * 1 + 360) % 360) - 270 = 0 degrees. + const int expectedQuarterTurns = _0DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + + testWidgets( + 'initial default display rotation is 90 degrees, then the preview Texture is rotated 90 degrees clockwise', + (WidgetTester tester) async { + // Set up test to use front camera, tell camera that handlesCropAndRotation is false, + // set camera initial default display rotation to 0 degrees. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: () => + Future.value(Surface.rotation90), + handlesCropAndRotation: false, + getUiOrientation: proxyGetUiOrientation); + + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((270 - 270 * 1 + 360) % 360) - 270 = -270 degrees clockwise = 90 degrees clockwise. + // 270 is used in this calculation for the device orientation because it is the counter-clockwise degrees of the + // default display rotation. + const int expectedQuarterTurns = _90DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + + testWidgets( + 'initial default display rotation is 180 degrees, then the preview Texture is rotated 180 degrees clockwise', + (WidgetTester tester) async { + // Set up test to use front camera, tell camera that handlesCropAndRotation is false, + // set camera initial default display rotation to 0 degrees. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: () => + Future.value(Surface.rotation180), + handlesCropAndRotation: false, + getUiOrientation: proxyGetUiOrientation); + + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((270 - 180 * 1 + 360) % 360) - 270 = -180 degrees clockwise = 180 degrees clockwise. + const int expectedQuarterTurns = _180DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + + testWidgets( + 'initial default display rotation is 270 degrees, then the preview Texture is rotated 270 degrees clockwise', + (WidgetTester tester) async { + // Set up test to use front camera, tell camera that handlesCropAndRotation is false, + // set camera initial default display rotation to 0 degrees. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: () => + Future.value(Surface.rotation270), + handlesCropAndRotation: false, + getUiOrientation: proxyGetUiOrientation); + + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((270 - 90 * 1 + 360) % 360) - 270 = -90 degrees clockwise = 270 degrees clockwise. + // 90 is used in this calculation for the device orientation because it is the counter-clockwise degrees of the + // default display rotation. + const int expectedQuarterTurns = _270DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + }); + + // Test that preview widgets responds as expected to the default display rotation changing: + testWidgets( + 'device orientation is landscapeRight, sensor orientation degrees is 270, camera is front facing, then the preview Texture rotates correctly as the default display rotation changes', + (WidgetTester tester) async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 11; + const DeviceOrientation testDeviceOrientation = + DeviceOrientation.landscapeRight; + + // Create and set up mock front camera CameraSelector, mock ProcessCameraProvider, 270 degree sensor orientation, + // media settings for test front camera. + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockCameraSelector Function( + // ignore: non_constant_identifier_names + {BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + LensFacing? requireLensFacing}) + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + final MockProcessCameraProvider mockProcessCameraProviderForFrontCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockFrontCameraSelector, + sensorRotationDegrees: 270); + const MediaSettings testMediaSettings = MediaSettings(); + + // Tell camera that handlesCropAndRotation is true, set camera initial device orientation + // to portrait down, set initial default display rotation to 0 degrees clockwise. + final MockDeviceOrientationManager mockDeviceOrientationManager = + MockDeviceOrientationManager(); + when(mockDeviceOrientationManager.getUiOrientation()).thenAnswer((_) => + Future.value( + _serializeDeviceOrientation(testDeviceOrientation))); + when(mockDeviceOrientationManager.getDefaultDisplayRotation()) + .thenAnswer((_) => Future.value(Surface.rotation0)); + camera.proxy = getProxyForCreatingTestCameraWithDeviceOrientationManager( + mockDeviceOrientationManager, + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + handlesCropAndRotation: false); + + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, testMediaSettings); + + // Calculated according to: ((270 - counterClockwiseDefaultDisplayRotation * 1 + 360) % 360) - 90. + // 90 is used in this calculation for the CameraPreview pre-applied rotation because it is the + // rotation that the CameraPreview widget aapplies based on the landscape right device orientation. + final Map expectedRotationPerDefaultDisplayRotation = + { + Surface.rotation0: _180DegreesClockwise, + Surface.rotation90: _270DegreesClockwise, + Surface.rotation180: _0DegreesClockwise, + Surface.rotation270: _90DegreesClockwise, + }; + + // Put camera preview in widget tree. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + for (final int currentDefaultDisplayRotation + in expectedRotationPerDefaultDisplayRotation.keys) { + // Modify CameraXProxy to return the default display rotation we want to test. + when(mockDeviceOrientationManager.getDefaultDisplayRotation()) + .thenAnswer((_) async => currentDefaultDisplayRotation); + + const DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(testDeviceOrientation); + AndroidCameraCameraX.deviceOrientationChangedStreamController + .add(testEvent); + + await tester.pumpAndSettle(); + + // Verify Texture is rotated by expected clockwise degrees. + final int expectedQuarterTurns = + expectedRotationPerDefaultDisplayRotation[ + currentDefaultDisplayRotation]!; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns < 0 + ? rotatedBox.quarterTurns + 4 + : rotatedBox.quarterTurns; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, reason: - 'When the device orientation is $currentDeviceOrientation, expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated ${rotatedBox.quarterTurns} quarter turns.', - ); - } + 'When the default display rotation is $currentDefaultDisplayRotation, expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated ${rotatedBox.quarterTurns} quarter turns.'); + } + }); - await AndroidCameraCameraX.deviceOrientationChangedStreamController - .close(); - }, - ); + // Test that preview widgets responds as expected to the device orientation changing: + testWidgets( + 'default display rotation is 90, sensor orientation degrees is 90, camera is front facing, then the preview Texture rotates correctly as the device orientation rotates', + (WidgetTester tester) async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 3372; + + // Create and set up mock CameraSelector and mock ProcessCameraProvider for test front camera + // with sensor orientation degrees 90. + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockCameraSelector Function({ + LensFacing? requireLensFacing, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + final MockProcessCameraProvider mockProcessCameraProviderForFrontCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockFrontCameraSelector, + sensorRotationDegrees: 90, + ); + + // Media settings to create camera; irrelevant for test. + const MediaSettings testMediaSettings = MediaSettings(); + + // Set up test to use front camera and tell camera that handlesCropAndRotation is false, + // set camera initial device orientation to landscape left, set initial default display + // rotation to 90 degrees clockwise. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + handlesCropAndRotation: false, + getUiOrientation: /* initial device orientation irrelevant for test */ + () async => + _serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + getDefaultDisplayRotation: () => + Future.value(Surface.rotation90)); + + // Get and create test front camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + + // Calculated according to: ((90 - 270 * 1 + 360) % 360) - cameraPreviewPreAppliedRotation. + // 270 is used in this calculation for the device orientation because it is the + // counter-clockwise degrees of the default display rotation. + final Map expectedRotationPerDeviceOrientation = + { + DeviceOrientation.portraitUp: _180DegreesClockwise, + DeviceOrientation.landscapeRight: _90DegreesClockwise, + DeviceOrientation.portraitDown: _0DegreesClockwise, + DeviceOrientation.landscapeLeft: _270DegreesClockwise, + }; + + // Put camera preview in widget tree. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + for (final DeviceOrientation currentDeviceOrientation + in expectedRotationPerDeviceOrientation.keys) { + final DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(currentDeviceOrientation); + AndroidCameraCameraX.deviceOrientationChangedStreamController.add( + testEvent, + ); + + await tester.pumpAndSettle(); + + // Verify Texture is rotated by expected clockwise degrees. + final int expectedQuarterTurns = + expectedRotationPerDeviceOrientation[currentDeviceOrientation]!; + final RotatedBox rotatedBox = tester.widget( + find.byType(RotatedBox), + ); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns < 0 + ? rotatedBox.quarterTurns + 4 + : rotatedBox.quarterTurns; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: + 'When the device orientation is $currentDeviceOrientation, expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated ${rotatedBox.quarterTurns} quarter turns.'); + } + }); // Test the preview rotation responds to the two most common sensor orientations for Android phone cameras; see // https://developer.android.com/media/camera/camera2/camera-preview#camera_orientation. group( - 'initial device orientation is DeviceOrientation.landscapeLeft, camera is back facing,', - () { - late AndroidCameraCameraX camera; - late int cameraId; - late MockCameraSelector mockBackCameraSelector; - late MockCameraSelector Function({ - LensFacing? requireLensFacing, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) proxyCreateCameraSelectorForBackCamera; - late MediaSettings testMediaSettings; - late DeviceOrientation testInitialDeviceOrientation; + 'initial device orientation is DeviceOrientation.landscapeLeft, initial default display rotation is 90, camera is back facing,', + () { + late AndroidCameraCameraX camera; + late int cameraId; + late MockCameraSelector mockBackCameraSelector; + late MockCameraSelector Function( + // ignore: non_constant_identifier_names + {BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + LensFacing? requireLensFacing}) + proxyCreateCameraSelectorForBackCamera; + late Future Function() proxyGetDefaultDisplayRotation; + late MediaSettings testMediaSettings; + late DeviceOrientation testInitialDeviceOrientation; - setUp(() { - camera = AndroidCameraCameraX(); - cameraId = 347; + setUp(() { + camera = AndroidCameraCameraX(); + cameraId = 347; - // Set test camera initial device orientation for test. - testInitialDeviceOrientation = DeviceOrientation.landscapeLeft; + // Set test camera initial device orientation for test. + testInitialDeviceOrientation = DeviceOrientation.landscapeLeft; - // Create and set up mock CameraSelector and mock ProcessCameraProvider for test back camera - // with sensor orientation degrees 270. - mockBackCameraSelector = MockCameraSelector(); - proxyCreateCameraSelectorForBackCamera = - createCameraSelectorForBackCamera(mockBackCameraSelector); + // Create and set up mock CameraSelector and mock ProcessCameraProvider for test back camera + // with sensor orientation degrees 270. + mockBackCameraSelector = MockCameraSelector(); + proxyCreateCameraSelectorForBackCamera = + createCameraSelectorForBackCamera(mockBackCameraSelector); + proxyGetDefaultDisplayRotation = + () => Future.value(Surface.rotation270); - testMediaSettings = const MediaSettings(); - }); + testMediaSettings = const MediaSettings(); + }); - testWidgets( - 'sensor orientation degrees is 90, then the preview Texture is rotated 90 degrees clockwise', + testWidgets( + 'sensor orientation degrees is 90, then the preview Texture is rotated 270 degrees clockwise', (WidgetTester tester) async { - // Create mock ProcessCameraProvider that will acknowledge that the test back camera with sensor orientation degrees - // 90 is available. - final MockProcessCameraProvider - mockProcessCameraProviderForBackCamera = - setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( - mockCameraSelector: mockBackCameraSelector, - sensorRotationDegrees: 90, - ); - - // Set up test to use back camera, tell camera that handlesCropAndRotation is false, - // set camera initial device orientation to landscape left. - camera.proxy = getProxyForCreatingTestCamera( - mockProcessCameraProvider: mockProcessCameraProviderForBackCamera, - createCameraSelector: proxyCreateCameraSelectorForBackCamera, - handlesCropAndRotation: false, - getUiOrientation: () async => - _serializeDeviceOrientation(testInitialDeviceOrientation), - ); - - // Get and create test back camera. - final List availableCameras = - await camera.availableCameras(); - expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); - - // Put camera preview in widget tree. - await tester.pumpWidget(camera.buildPreview(cameraId)); - - // Verify Texture is rotated by ((90 - 270 * -1 + 360) % 360) - 270 = -270 degrees clockwise = 270 degrees counterclockwise = 90 degrees clockwise. - const int expectedQuarterTurns = _90DegreesClockwise; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); - expect( - clockwiseQuarterTurns, - expectedQuarterTurns, - reason: getExpectedRotationTestFailureReason( - expectedQuarterTurns, - rotatedBox.quarterTurns, - ), - ); - }, + // Create mock ProcessCameraProvider that will acknowledge that the test back camera with sensor orientation degrees + // 90 is available. + final MockProcessCameraProvider mockProcessCameraProviderForBackCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockBackCameraSelector, + sensorRotationDegrees: 90, ); - testWidgets( - 'sensor orientation degrees is 270, then the preview Texture is rotated 270 degrees clockwise', + // Set up test to use back camera, tell camera that handlesCropAndRotation is false, + // set camera initial device orientation to landscape left. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForBackCamera, + createCameraSelector: proxyCreateCameraSelectorForBackCamera, + getDefaultDisplayRotation: proxyGetDefaultDisplayRotation, + handlesCropAndRotation: false, + getUiOrientation: () async => + _serializeDeviceOrientation(testInitialDeviceOrientation)); + + // Get and create test back camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((90 - 90 * -1 + 360) % 360) - 270 = -90 degrees clockwise = 270 degrees clockwise. + // 90 is used in this calculation for the device orientation because it is the counter-clockwise degrees of the + // default display rotation. + const int expectedQuarterTurns = _270DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + + testWidgets( + 'sensor orientation degrees is 270, then the preview Texture is rotated 90 degrees clockwise', (WidgetTester tester) async { - // Create mock ProcessCameraProvider that will acknowledge that the test back camera with sensor orientation degrees - // 270 is available. - final MockProcessCameraProvider - mockProcessCameraProviderForBackCamera = - setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( - mockCameraSelector: mockBackCameraSelector, - sensorRotationDegrees: 270, - ); - - // Set up test to use back camera, tell camera that handlesCropAndRotation is false, - // set camera initial device orientation to landscape left. - camera.proxy = getProxyForCreatingTestCamera( - mockProcessCameraProvider: mockProcessCameraProviderForBackCamera, - createCameraSelector: proxyCreateCameraSelectorForBackCamera, - handlesCropAndRotation: false, - getUiOrientation: () async => - _serializeDeviceOrientation(testInitialDeviceOrientation), - ); - - // Get and create test back camera. - final List availableCameras = - await camera.availableCameras(); - expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); - - // Put camera preview in widget tree. - await tester.pumpWidget(camera.buildPreview(cameraId)); - - // Verify Texture is rotated by ((270 - 270 * -1 + 360) % 360) - 270 = -90 degrees clockwise = 90 degrees counterclockwise = 270 degrees clockwise. - const int expectedQuarterTurns = _270DegreesClockwise; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); - expect( - clockwiseQuarterTurns, - expectedQuarterTurns, - reason: getExpectedRotationTestFailureReason( - expectedQuarterTurns, - rotatedBox.quarterTurns, - ), - ); - }, + // Create mock ProcessCameraProvider that will acknowledge that the test back camera with sensor orientation degrees + // 270 is available. + final MockProcessCameraProvider mockProcessCameraProviderForBackCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockBackCameraSelector, + sensorRotationDegrees: 270, ); - }, - ); + + // Set up test to use back camera, tell camera that handlesCropAndRotation is false, + // set camera initial device orientation to landscape left. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForBackCamera, + createCameraSelector: proxyCreateCameraSelectorForBackCamera, + getDefaultDisplayRotation: proxyGetDefaultDisplayRotation, + handlesCropAndRotation: false, + getUiOrientation: () async => + _serializeDeviceOrientation(testInitialDeviceOrientation)); + + // Get and create test back camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((270 - 90 * -1 + 360) % 360) - 270 = -270 degrees clockwise = 90 degrees clockwise. + // 90 is used in this calculation for the device orientation because it is the counter-clockwise degrees of the + // default display rotation. + const int expectedQuarterTurns = _90DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + }); // Test the preview rotation responds to the camera being front or back facing: group( - 'initial device orientation is DeviceOrientation.landscapeRight, sensor orientation degrees is 90,', - () { - late AndroidCameraCameraX camera; - late int cameraId; - late MediaSettings testMediaSettings; - late DeviceOrientation testInitialDeviceOrientation; - late int testSensorOrientation; - - setUp(() { - camera = AndroidCameraCameraX(); - cameraId = 317; - - // Set test camera initial device orientation and sensor orientation for test. - testInitialDeviceOrientation = DeviceOrientation.landscapeRight; - testSensorOrientation = 90; - - // Media settings to create camera; irrelevant for test. - testMediaSettings = const MediaSettings(); - }); - - testWidgets( - 'camera is front facing, then the preview Texture is rotated 270 degrees clockwise', + 'initial device orientation is DeviceOrientation.landscapeRight, initial default displauy rotation is 0 degrees, sensor orientation degrees is 90,', + () { + late AndroidCameraCameraX camera; + late int cameraId; + late DeviceOrientation testInitialDeviceOrientation; + late int testSensorOrientation; + late Future Function() proxyGetDefaultDisplayRotation; + late MediaSettings testMediaSettings; + + setUp(() { + camera = AndroidCameraCameraX(); + cameraId = 317; + + // Set test camera initial device orientation and sensor orientation for test. + testInitialDeviceOrientation = DeviceOrientation.landscapeRight; + testSensorOrientation = 90; + + // Create mock for seting initial default display rotation to 180 degrees. + proxyGetDefaultDisplayRotation = + () => Future.value(Surface.rotation90); + + // Media settings to create camera; irrelevant for test. + testMediaSettings = const MediaSettings(); + }); + + testWidgets( + 'camera is front facing, then the preview Texture is rotated 90 degrees clockwise', (WidgetTester tester) async { - // Set up test front camera with sensor orientation degrees 90. - final MockCameraSelector mockFrontCameraSelector = - MockCameraSelector(); - final MockProcessCameraProvider mockProcessCameraProvider = - setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( - mockCameraSelector: mockFrontCameraSelector, - sensorRotationDegrees: testSensorOrientation, - ); + // Set up test front camera with sensor orientation degrees 90. + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockProcessCameraProvider mockProcessCameraProvider = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockFrontCameraSelector, + sensorRotationDegrees: testSensorOrientation, + ); + // Set up front camera selection and initial device orientation as landscape right. + final MockCameraSelector Function( + // ignore: non_constant_identifier_names + {BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + LensFacing? requireLensFacing}) + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: proxyGetDefaultDisplayRotation, + handlesCropAndRotation: false, + getUiOrientation: () async => + _serializeDeviceOrientation(testInitialDeviceOrientation)); - // Set up front camera selection and initial device orientation as landscape right. - final MockCameraSelector Function({ - LensFacing? requireLensFacing, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) proxyCreateCameraSelectorForFrontCamera = - createCameraSelectorForFrontCamera(mockFrontCameraSelector); - camera.proxy = getProxyForCreatingTestCamera( - mockProcessCameraProvider: mockProcessCameraProvider, - createCameraSelector: proxyCreateCameraSelectorForFrontCamera, - handlesCropAndRotation: false, - getUiOrientation: () async => - _serializeDeviceOrientation(testInitialDeviceOrientation), - ); - - // Get and create test camera. - final List availableCameras = - await camera.availableCameras(); - expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); - - // Put camera preview in widget tree. - await tester.pumpWidget(camera.buildPreview(cameraId)); - - // Verify Texture is rotated by ((90 - 90 * 1 + 360) % 360) - 90 = -90 degrees clockwise = 90 degrees counterclockwise = 270 degrees clockwise. - const int expectedQuarterTurns = _270DegreesClockwise; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); - expect( - clockwiseQuarterTurns, - expectedQuarterTurns, - reason: getExpectedRotationTestFailureReason( - expectedQuarterTurns, - rotatedBox.quarterTurns, - ), - ); - }, + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, + testMediaSettings, ); - testWidgets( - 'camera is back facing, then the preview Texture is rotated 90 degrees clockwise', + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((90 - 270 * 1 + 360) % 360) - 90 = 90 degrees clockwise. + // 270 is used in this calculation for the device orientation because it is the counter-clockwise degrees of the + // default display rotation. + const int expectedQuarterTurns = _90DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(rotatedBox.quarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + + testWidgets( + 'camera is back facing, then the preview Texture is rotated 270 degrees clockwise', (WidgetTester tester) async { - // Set up test front camera with sensor orientation degrees 90. - final MockCameraSelector mockBackCameraSelector = - MockCameraSelector(); - final MockProcessCameraProvider mockProcessCameraProvider = - setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( - mockCameraSelector: mockBackCameraSelector, - sensorRotationDegrees: testSensorOrientation, - ); - - // Set up front camera selection and initial device orientation as landscape right. - final MockCameraSelector Function({ - LensFacing? requireLensFacing, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) proxyCreateCameraSelectorForFrontCamera = - createCameraSelectorForBackCamera(mockBackCameraSelector); - camera.proxy = getProxyForCreatingTestCamera( - mockProcessCameraProvider: mockProcessCameraProvider, - createCameraSelector: proxyCreateCameraSelectorForFrontCamera, - handlesCropAndRotation: false, - getUiOrientation: () async => - _serializeDeviceOrientation(testInitialDeviceOrientation), - ); - - // Get and create test camera. - final List availableCameras = - await camera.availableCameras(); - expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); - - // Put camera preview in widget tree. - await tester.pumpWidget(camera.buildPreview(cameraId)); - - // Verify Texture is rotated by ((90 - 90 * -1 + 360) % 360) - 90 = 90 degrees clockwise. - const int expectedQuarterTurns = _90DegreesClockwise; - final RotatedBox rotatedBox = tester.widget( - find.byType(RotatedBox), - ); - expect(rotatedBox.child, isA()); - expect((rotatedBox.child! as Texture).textureId, cameraId); - expect( - rotatedBox.quarterTurns, - expectedQuarterTurns, - reason: getExpectedRotationTestFailureReason( - expectedQuarterTurns, - rotatedBox.quarterTurns, - ), - ); - }, + // Set up test front camera with sensor orientation degrees 90. + final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); + final MockProcessCameraProvider mockProcessCameraProvider = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockBackCameraSelector, + sensorRotationDegrees: testSensorOrientation, ); - }, - ); - }); -} -String _serializeDeviceOrientation(DeviceOrientation orientation) { - switch (orientation) { - case DeviceOrientation.portraitUp: - return 'PORTRAIT_UP'; - case DeviceOrientation.landscapeLeft: - return 'LANDSCAPE_LEFT'; - case DeviceOrientation.portraitDown: - return 'PORTRAIT_DOWN'; - case DeviceOrientation.landscapeRight: - return 'LANDSCAPE_RIGHT'; - } + // Set up front camera selection and initial device orientation as landscape right. + final MockCameraSelector Function( + // ignore: non_constant_identifier_names + {BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + LensFacing? requireLensFacing}) + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForBackCamera(mockBackCameraSelector); + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + getDefaultDisplayRotation: proxyGetDefaultDisplayRotation, + handlesCropAndRotation: false, + getUiOrientation: () async => + _serializeDeviceOrientation(testInitialDeviceOrientation)); + + // Get and create test camera. + final List availableCameras = + await camera.availableCameras(); + expect(availableCameras.length, 1); + await camera.createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + + // Put camera preview in widget tree and pump one frame so that Future to retrieve + // the initial default display rotation completes. + await tester.pumpWidget(camera.buildPreview(cameraId)); + await tester.pump(); + + // Verify Texture is rotated by ((90 - 270 * -1 + 360) % 360) - 90 = -90 degrees clockwise = 270 degrees clockwise. + // 270 is used in this calculation for the device orientation because it is the counter-clockwise degrees of the + // default display rotation. + const int expectedQuarterTurns = _270DegreesClockwise; + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + }); + }); }