diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index bcec28fe5b9..73359e455ad 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -140,7 +140,7 @@ class FakeController extends ValueNotifier void main() { group('RotatedBox (Android only)', () { testWidgets( - 'when recording rotatedBox should turn according to recording orientation', + 'when recording in DeviceOrientaiton.portraitUp, rotatedBox should not be rotated', ( WidgetTester tester, ) async { @@ -148,17 +148,129 @@ void main() { final FakeController controller = FakeController(); addTearDown(controller.dispose); + controller.value = controller.value.copyWith( - isInitialized: true, - isRecordingVideo: true, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: - const Optional.fromNullable( - DeviceOrientation.landscapeRight), - recordingOrientation: const Optional.fromNullable( - DeviceOrientation.landscapeLeft), - previewSize: const Size(480, 640), + isInitialized: true, + isRecordingVideo: true, + deviceOrientation: DeviceOrientation.portraitDown, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.portraitUp), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 0); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when recording in DeviceOrientaiton.landscapeRight, rotatedBox should be rotated by one clockwise quarter turn', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + isRecordingVideo: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeRight), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 1); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when recording in DeviceOrientaiton.portraitDown, rotatedBox should be rotated by two clockwise quarter turns', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + isRecordingVideo: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.portraitDown), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 2); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when recording in DeviceOrientaiton.landscapeLeft, rotatedBox should be rotated by three clockwise quarter turns', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + isRecordingVideo: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); await tester.pumpWidget( Directionality( @@ -176,7 +288,7 @@ void main() { }); testWidgets( - 'when orientation locked rotatedBox should turn according to locked orientation', + 'when orientation locked in DeviceOrientaiton.portaitUp, rotatedBox should not be rotated', ( WidgetTester tester, ) async { @@ -184,16 +296,53 @@ void main() { final FakeController controller = FakeController(); addTearDown(controller.dispose); + controller.value = controller.value.copyWith( - isInitialized: true, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: - const Optional.fromNullable( - DeviceOrientation.landscapeRight), - recordingOrientation: const Optional.fromNullable( - DeviceOrientation.landscapeLeft), - previewSize: const Size(480, 640), + isInitialized: true, + deviceOrientation: DeviceOrientation.portraitDown, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.portraitUp), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 0); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when orientation locked in DeviceOrientaiton.landscapeRight, rotatedBox should be rotated by one clockwise quarter turn', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + deviceOrientation: DeviceOrientation.portraitDown, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); await tester.pumpWidget( Directionality( @@ -211,7 +360,7 @@ void main() { }); testWidgets( - 'when not locked and not recording rotatedBox should turn according to device orientation', + 'when orientation locked in DeviceOrientaiton.portraitDown, rotatedBox should be rotated by two clockwise quarter turns', ( WidgetTester tester, ) async { @@ -219,13 +368,86 @@ void main() { final FakeController controller = FakeController(); addTearDown(controller.dispose); + controller.value = controller.value.copyWith( - isInitialized: true, - deviceOrientation: DeviceOrientation.portraitUp, - recordingOrientation: const Optional.fromNullable( - DeviceOrientation.landscapeLeft), - previewSize: const Size(480, 640), + isInitialized: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.portraitDown), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 2); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when orientation locked in DeviceOrientaiton.landscapeRight, rotatedBox should be rotated by three clockwise quarter turns', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 1); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when orientation not locked, not recording, and device orientation is portrait up, rotatedBox should not be rotated', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + deviceOrientation: DeviceOrientation.portraitUp, + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); await tester.pumpWidget( Directionality( @@ -241,6 +463,105 @@ void main() { debugDefaultTargetPlatformOverride = null; }); + + testWidgets( + 'when orientation not locked, not recording, and device orientation is landscape right, rotatedBox should be rotated by one clockwise quarter turn', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + deviceOrientation: DeviceOrientation.landscapeRight, + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 1); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when orientation not locked, not recording, and device orientation is portrait down, rotatedBox should be rotated by two clockwise quarter turns', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + deviceOrientation: DeviceOrientation.portraitDown, + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 2); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when orientation not locked, not recording, and device orientation is landscape left, rotatedBox should be rotated by three clockwise quarter turns', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + addTearDown(controller.dispose); + + controller.value = controller.value.copyWith( + isInitialized: true, + deviceOrientation: DeviceOrientation.landscapeLeft, + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.portraitDown), + previewSize: const Size(480, 640) // preview size irrelevant to test + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + final RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 3); + + debugDefaultTargetPlatformOverride = null; + }); }, skip: kIsWeb); testWidgets('when not on Android there should not be a rotated box', @@ -249,9 +570,9 @@ void main() { final FakeController controller = FakeController(); addTearDown(controller.dispose); controller.value = controller.value.copyWith( - isInitialized: true, - previewSize: const Size(480, 640), - ); + isInitialized: true, + previewSize: const Size(480, 640) // preview size irrelevant to test + ); await tester.pumpWidget( Directionality( diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 7e190cb5ce3..0328dff3d54 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.14 + +* Fixes incorrect camera preview rotation. + ## 0.6.13 * Adds API support query for image streaming. diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 7adf02595ad..93f65cbc791 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -1441,9 +1441,6 @@ void requestCameraPermissions( @NonNull String getTempFilePath(@NonNull String prefix, @NonNull String suffix); - @NonNull - Boolean isPreviewPreTransformed(); - /** The codec used by SystemServicesHostApi. */ static @NonNull MessageCodec getCodec() { return SystemServicesHostApiCodec.INSTANCE; @@ -1511,29 +1508,6 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.SystemServicesHostApi.isPreviewPreTransformed", - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList(); - try { - Boolean output = api.isPreviewPreTransformed(); - wrapped.add(0, output); - } catch (Throwable exception) { - ArrayList wrappedError = wrapError(exception); - wrapped = wrappedError; - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ @@ -1761,6 +1735,9 @@ void create( void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation); + @NonNull + Boolean surfaceProducerHandlesCropAndRotation(); + /** The codec used by PreviewHostApi. */ static @NonNull MessageCodec getCodec() { return PreviewHostApiCodec.INSTANCE; @@ -1898,6 +1875,29 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PreviewHos channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PreviewHostApi.surfaceProducerHandlesCropAndRotation", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + Boolean output = api.surfaceProducerHandlesCropAndRotation(); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java index 92202dd74dc..ffec50a22d4 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java @@ -155,7 +155,10 @@ String getProvideSurfaceErrorDescription(int resultCode) { public void releaseFlutterSurfaceTexture() { if (flutterSurfaceProducer != null) { flutterSurfaceProducer.release(); + return; } + throw new IllegalStateException( + "releaseFlutterSurfaceTexture() cannot be called if the flutterSurfaceProducer for the camera preview has not yet been initialized."); } /** Returns the resolution information for the specified {@link Preview}. */ @@ -179,6 +182,16 @@ public void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation) preview.setTargetRotation(rotation.intValue()); } + @NonNull + @Override + public Boolean surfaceProducerHandlesCropAndRotation() { + if (flutterSurfaceProducer != null) { + return flutterSurfaceProducer.handlesCropAndRotation(); + } + throw new IllegalStateException( + "surfaceProducerHandlesCropAndRotation() cannot be called if the flutterSurfaceProducer for the camera preview has not yet been initialized."); + } + /** Retrieves the {@link Preview} instance associated with the specified {@code identifier}. */ private Preview getPreviewInstance(@NonNull Long identifier) { return Objects.requireNonNull(instanceManager.getInstance(identifier)); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java index 138e9259e02..d058d62fe22 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java @@ -6,7 +6,6 @@ import android.app.Activity; import android.content.Context; -import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -104,18 +103,4 @@ public String getTempFilePath(@NonNull String prefix, @NonNull String suffix) { null); } } - - /** - * Returns whether or not Impeller uses an {@code ImageReader} backend to provide a {@code - * Surface} to CameraX to build the preview. If it is backed by an {@code ImageReader}, then - * CameraX will not automatically apply the transformation needed to correct the preview. - * - *

This is determined by the engine, which approximately uses {@code SurfaceTexture}s on - * Android SDKs below 29. - */ - @Override - @NonNull - public Boolean isPreviewPreTransformed() { - return Build.VERSION.SDK_INT < 29; - } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java index 6fd43c2f9a3..bc2577bc007 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java @@ -263,6 +263,20 @@ public void setTargetRotation_makesCallToSetTargetRotation() { verify(mockPreview).setTargetRotation(targetRotation); } + @Test + public void + surfaceProducerHandlesCropAndRotation_returnsIfSurfaceProducerHandlesCropAndRotation() { + final PreviewHostApiImpl hostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final TextureRegistry.SurfaceProducer mockSurfaceProducer = + mock(TextureRegistry.SurfaceProducer.class); + + hostApi.flutterSurfaceProducer = mockSurfaceProducer; + when(mockSurfaceProducer.handlesCropAndRotation()).thenReturn(true); + + assertEquals(hostApi.surfaceProducerHandlesCropAndRotation(), true); + } + // TODO(bparrishMines): Replace with inline calls to onSurfaceCleanup once available on stable; // see https://github.com/flutter/flutter/issues/16125. This separate method only exists to scope // the suppression. diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java index 52d02e67f65..fdfc1b224ff 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java @@ -5,9 +5,7 @@ package io.flutter.plugins.camerax; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -33,7 +31,6 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) public class SystemServicesTest { @@ -136,28 +133,4 @@ public void getTempFilePath_throwsRuntimeExceptionOnIOException() { mockedStaticFile.close(); } - - @Test - @Config(sdk = 28) - public void isPreviewPreTransformed_returnsTrueWhenRunningBelowSdk29() { - final SystemServicesHostApiImpl systemServicesHostApi = - new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); - assertTrue(systemServicesHostApi.isPreviewPreTransformed()); - } - - @Test - @Config(sdk = 28) - public void isPreviewPreTransformed_returnsTrueWhenRunningSdk28() { - final SystemServicesHostApiImpl systemServicesHostApi = - new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); - assertTrue(systemServicesHostApi.isPreviewPreTransformed()); - } - - @Test - @Config(sdk = 29) - public void isPreviewPreTransformed_returnsFalseWhenRunningAboveSdk28() { - final SystemServicesHostApiImpl systemServicesHostApi = - new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); - assertFalse(systemServicesHostApi.isPreviewPreTransformed()); - } } 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 31cde2fa0fc..8e61727cc61 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 @@ -47,6 +47,7 @@ import 'recording.dart'; import 'resolution_filter.dart'; import 'resolution_selector.dart'; import 'resolution_strategy.dart'; +import 'rotated_preview.dart'; import 'surface.dart'; import 'system_services.dart'; import 'use_case.dart'; @@ -238,12 +239,18 @@ class AndroidCameraCameraX extends CameraPlatform { late bool cameraIsFrontFacing; /// The camera sensor orientation. + /// + /// This can change if the camera being used changes. Also, it is independent + /// of the device orientation or user interface orientation. @visibleForTesting - late int sensorOrientation; + late double sensorOrientationDegrees; + + /// Whether or not the Android surface producer automatically handles + /// correcting the rotation of camera previews for the device this plugin runs on. + late bool _handlesCropAndRotation; - /// Subscription for listening to changes in device orientation. - StreamSubscription? - _subscriptionForDeviceOrientationChanges; + /// The initial orientation of the device when the camera is created. + late DeviceOrientation _initialDeviceOrientation; /// Returns list of all available cameras and their descriptions. @override @@ -380,10 +387,10 @@ class AndroidCameraCameraX extends CameraPlatform { // Retrieve info required for correcting the rotation of the camera preview // if necessary. - - final Camera2CameraInfo camera2CameraInfo = - await proxy.getCamera2CameraInfo(cameraInfo!); - sensorOrientation = await proxy.getSensorOrientation(camera2CameraInfo); + sensorOrientationDegrees = cameraDescription.sensorOrientation.toDouble(); + _handlesCropAndRotation = + await proxy.previewSurfaceProducerHandlesCropAndRotation(preview!); + _initialDeviceOrientation = await proxy.getUiOrientation(); return flutterSurfaceTextureId; } @@ -444,7 +451,6 @@ class AndroidCameraCameraX extends CameraPlatform { await liveCameraState?.removeObservers(); processCameraProvider?.unbindAll(); await imageAnalysis?.clearAnalyzer(); - await _subscriptionForDeviceOrientationChanges?.cancel(); } /// The camera has been initialized. @@ -836,7 +842,30 @@ class AndroidCameraCameraX extends CameraPlatform { ); } - return Texture(textureId: cameraId); + 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, + sensorOrientationDegrees: sensorOrientationDegrees, + child: preview, + ); + } } /// Captures an image and returns the file where it was saved. diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index a9461eaaae0..e830a288ce9 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -1003,33 +1003,6 @@ class SystemServicesHostApi { return (replyList[0] as String?)!; } } - - Future isPreviewPreTransformed() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesHostApi.isPreviewPreTransformed', - codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { - throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], - ); - } else if (replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyList[0] as bool?)!; - } - } } abstract class SystemServicesFlutterApi { @@ -1356,6 +1329,33 @@ class PreviewHostApi { return; } } + + Future surfaceProducerHandlesCropAndRotation() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.surfaceProducerHandlesCropAndRotation', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } } class VideoCaptureHostApi { diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart index feae868ac40..c5d92d29560 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart @@ -70,6 +70,8 @@ class CameraXProxy { this.getCamera2CameraInfo = _getCamera2CameraInfo, this.getUiOrientation = _getUiOrientation, this.getSensorOrientation = _getSensorOrientation, + this.previewSurfaceProducerHandlesCropAndRotation = + _previewSurfaceProducerHandlesCropAndRotation, }); /// Returns a [ProcessCameraProvider] instance. @@ -200,6 +202,11 @@ class CameraXProxy { Future Function(Camera2CameraInfo camera2CameraInfo) getSensorOrientation; + /// Returns whether or not the preview's surface producer handles correctly + /// rotating the camera preview automatically. + Future Function(Preview preview) + previewSurfaceProducerHandlesCropAndRotation; + static Future _getProcessCameraProvider() { return ProcessCameraProvider.getInstance(); } @@ -355,4 +362,9 @@ class CameraXProxy { Camera2CameraInfo camera2CameraInfo) async { return camera2CameraInfo.getSensorOrientation(); } + + static Future _previewSurfaceProducerHandlesCropAndRotation( + Preview preview) async { + return preview.surfaceProducerHandlesCropAndRotation(); + } } diff --git a/packages/camera/camera_android_camerax/lib/src/preview.dart b/packages/camera/camera_android_camerax/lib/src/preview.dart index 8990313817b..76da65e4a81 100644 --- a/packages/camera/camera_android_camerax/lib/src/preview.dart +++ b/packages/camera/camera_android_camerax/lib/src/preview.dart @@ -86,6 +86,12 @@ class Preview extends UseCase { Future getResolutionInfo() { return _api.getResolutionInfoFromInstance(this); } + + /// Returns whether or not the Android surface producer automatically handles + /// correcting the rotation of camera previews for the device this plugin runs on. + Future surfaceProducerHandlesCropAndRotation() { + return _api.surfaceProducerHandlesCropAndRotationFromInstance(); + } } /// Host API implementation of [Preview]. @@ -156,4 +162,10 @@ class PreviewHostApiImpl extends PreviewHostApi { return resolutionInfo; } + + /// Returns whether or not the Android surface producer automatically handles + /// correcting the rotation of camera previews for the device this plugin runs on. + Future surfaceProducerHandlesCropAndRotationFromInstance() { + return surfaceProducerHandlesCropAndRotation(); + } } diff --git a/packages/camera/camera_android_camerax/lib/src/rotated_preview.dart b/packages/camera/camera_android_camerax/lib/src/rotated_preview.dart new file mode 100644 index 00000000000..849de39013f --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/rotated_preview.dart @@ -0,0 +1,119 @@ +// 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'; + +/// Widget that rotates the camera preview to be upright according to the +/// current user interface orientation. +@internal +final class RotatedPreview extends StatefulWidget { + /// Creates [RotatedPreview] that will correct the preview + /// rotation assuming that the front camera is being used. + const RotatedPreview.frontFacingCamera( + this.initialDeviceOrientation, + this.deviceOrientation, { + required this.sensorOrientationDegrees, + required this.child, + super.key, + }) : facingSign = 1; + + /// Creates [RotatedPreview] that will correct the preview + /// rotation assuming that the back camera is being used. + const RotatedPreview.backFacingCamera( + this.initialDeviceOrientation, + this.deviceOrientation, { + required this.child, + required this.sensorOrientationDegrees, + super.key, + }) : facingSign = -1; + + /// The initial orientation of the device when the camera is created. + final DeviceOrientation initialDeviceOrientation; + + /// The orientation of the device using the camera. + final Stream deviceOrientation; + + /// The orienation of the camera sensor in degrees. + final double sensorOrientationDegrees; + + /// The camera preview [Widget] to rotate. + final Widget child; + + /// 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; + + @override + State createState() => _RotatedPreviewState(); +} + +final class _RotatedPreviewState extends State { + late DeviceOrientation deviceOrientation; + late StreamSubscription deviceOrientationSubscription; + + @override + void initState() { + deviceOrientation = widget.initialDeviceOrientation; + 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; + }); + }); + super.initState(); + } + + double _computeRotationDegrees( + DeviceOrientation orientation, { + required double sensorOrientationDegrees, + required int sign, + }) { + final double deviceOrientationDegrees = switch (orientation) { + DeviceOrientation.portraitUp => 0, + DeviceOrientation.landscapeRight => 90, + DeviceOrientation.portraitDown => 180, + DeviceOrientation.landscapeLeft => 270, + }; + + // Rotate the camera preview according to + // https://developer.android.com/media/camera/camera2/camera-preview#orientation_calculation. + double rotationDegrees = + (sensorOrientationDegrees - deviceOrientationDegrees * sign + 360) % + 360; + + // Then, subtract the rotation already applied in the CameraPreview widget + // (see camera/camera/lib/src/camera_preview.dart). + rotationDegrees -= deviceOrientationDegrees; + + return rotationDegrees; + } + + @override + void dispose() { + deviceOrientationSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final double rotationDegrees = _computeRotationDegrees( + deviceOrientation, + sensorOrientationDegrees: widget.sensorOrientationDegrees, + sign: widget.facingSign, + ); + return RotatedBox( + quarterTurns: rotationDegrees ~/ 90, + child: widget.child, + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart index 5f59bd2f4c6..b75a1cb9803 100644 --- a/packages/camera/camera_android_camerax/lib/src/system_services.dart +++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart @@ -48,21 +48,6 @@ class SystemServices { SystemServicesHostApi(binaryMessenger: binaryMessenger); return api.getTempFilePath(prefix, suffix); } - - /// Returns whether or not the Android Surface used to display the camera - /// preview is backed by a SurfaceTexture, to which the transformation to - /// correctly rotate the preview has been applied. - /// - /// This is used to determine the correct rotation of the camera preview - /// because Surfaces not backed by a SurfaceTexture are not transformed by - /// CameraX to the expected rotation based on that of the device and must - /// be corrected by the plugin. - static Future isPreviewPreTransformed( - {BinaryMessenger? binaryMessenger}) { - final SystemServicesHostApi api = - SystemServicesHostApi(binaryMessenger: binaryMessenger); - return api.isPreviewPreTransformed(); - } } /// Host API implementation of [SystemServices]. diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 949830db3e8..a6e4eded23a 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -260,8 +260,6 @@ abstract class SystemServicesHostApi { CameraPermissionsErrorData? requestCameraPermissions(bool enableAudio); String getTempFilePath(String prefix, String suffix); - - bool isPreviewPreTransformed(); } @FlutterApi() @@ -297,6 +295,8 @@ abstract class PreviewHostApi { ResolutionInfo getResolutionInfo(int identifier); void setTargetRotation(int identifier, int rotation); + + bool surfaceProducerHandlesCropAndRotation(); } @HostApi(dartHostTestHandler: 'TestVideoCaptureHostApi') diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index fb7e6f0e317..6f2b154a010 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.13 +version: 0.6.14 environment: sdk: ^3.6.0 diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index 89e201b3b32..2e4a5c83af7 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -50,7 +50,7 @@ import 'package:camera_android_camerax/src/zoom_state.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/services.dart' show DeviceOrientation, PlatformException, Uint8List; -import 'package:flutter/widgets.dart' show BuildContext, Size, Texture, Widget; +import 'package:flutter/widgets.dart' show BuildContext, Size; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -201,6 +201,8 @@ void main() { Future.value(MockCamera2CameraInfo()), getUiOrientation: () => Future.value(DeviceOrientation.portraitUp), + previewSurfaceProducerHandlesCropAndRotation: (_) => + Future.value(false), ); /// CameraXProxy for testing exposure and focus related controls. @@ -919,7 +921,9 @@ void main() { expect(camera.recorder!.qualitySelector, isNull); }); - test('createCamera sets sensor orientation as expected', () async { + test( + 'createCamera sets sensor orientation, handlesCropAndRotation, initialDeviceOrientation as expected', + () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); const CameraLensDirection testLensDirection = CameraLensDirection.back; const int testSensorOrientation = 270; @@ -929,6 +933,7 @@ void main() { sensorOrientation: testSensorOrientation); const bool enableAudio = true; const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const bool testHandlesCropAndRotation = true; const DeviceOrientation testUiOrientation = DeviceOrientation.portraitDown; // Mock/Detached objects for (typically attached) objects created by @@ -948,6 +953,8 @@ void main() { getProxyForTestingResolutionPreset(mockProcessCameraProvider); camera.proxy.getSensorOrientation = (_) async => Future.value(testSensorOrientation); + camera.proxy.previewSurfaceProducerHandlesCropAndRotation = + (_) async => Future.value(testHandlesCropAndRotation); camera.proxy.getUiOrientation = () async => Future.value(testUiOrientation); @@ -960,7 +967,7 @@ void main() { await camera.createCamera(testCameraDescription, testResolutionPreset, enableAudio: enableAudio); - expect(camera.sensorOrientation, testSensorOrientation); + expect(camera.sensorOrientationDegrees, testSensorOrientation); }); test( @@ -1296,6 +1303,8 @@ void main() { expect(camera.cameraControl, equals(mockCameraControl)); }); + // Further `buildPreview` testing concerning the Widget that it returns is + // located in preview_rotation_test.dart. test( 'buildPreview throws an exception if the preview is not bound to the lifecycle', () async { @@ -1310,22 +1319,6 @@ void main() { () => camera.buildPreview(cameraId), throwsA(isA())); }); - test( - 'buildPreview returns a Texture once the preview is bound to the lifecycle if it is backed by a SurfaceTexture', - () async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); - const int cameraId = 37; - - // Tell camera that createCamera has been called and thus, preview has been - // bound to the lifecycle of the camera. - camera.previewInitiallyBound = true; - - final Widget widget = camera.buildPreview(cameraId); - - expect(widget is Texture, isTrue); - expect((widget as Texture).textureId, cameraId); - }); - group('video recording', () { test( 'startVideoCapturing binds video capture use case, updates saved camera instance and its properties, and starts the recording', diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart index 316b1275969..fc838a461bb 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -1134,6 +1134,17 @@ class MockPreview extends _i1.Mock implements _i35.Preview { ), )), ) as _i17.Future<_i10.ResolutionInfo>); + + @override + _i17.Future surfaceProducerHandlesCropAndRotation() => + (super.noSuchMethod( + Invocation.method( + #surfaceProducerHandlesCropAndRotation, + [], + ), + returnValue: _i17.Future.value(false), + returnValueForMissingStub: _i17.Future.value(false), + ) as _i17.Future); } /// A class which mocks [ProcessCameraProvider]. @@ -1410,16 +1421,6 @@ class MockTestSystemServicesHostApi extends _i1.Mock ), ), ) as String); - - @override - bool isPreviewPreTransformed() => (super.noSuchMethod( - Invocation.method( - #isPreviewPreTransformed, - [], - ), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); } /// A class which mocks [VideoCapture]. diff --git a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart new file mode 100644 index 00000000000..e06d16cbc0f --- /dev/null +++ b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart @@ -0,0 +1,637 @@ +// 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:camera_android_camerax/camera_android_camerax.dart'; +import 'package:camera_android_camerax/src/camera_selector.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/camerax_proxy.dart'; +import 'package:camera_android_camerax/src/device_orientation_manager.dart'; +import 'package:camera_android_camerax/src/fallback_strategy.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import 'android_camera_camerax_test.mocks.dart'; + +// Constants to map clockwise degree rotations to quarter turns: +const int _90DegreesClockwise = 1; +const int _270DegreesClockwise = 3; + +void main() { + /// Sets up mock CameraSelector and mock ProcessCameraProvider used to + /// select test camera when `availableCameras` is called. + /// + /// Also mocks a call for mock ProcessCameraProvider that is irrelevant + /// to this test. + /// + /// Returns mock ProcessCameraProvider that is used to select test camera. + MockProcessCameraProvider + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + {required MockCameraSelector mockCameraSelector, + required int sensorRotationDegrees}) { + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCamera mockCamera = MockCamera(); + + // Mock retrieving available test camera. + when(mockProcessCameraProvider.bindToLifecycle(any, any)) + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockProcessCameraProvider.getAvailableCameraInfos()) + .thenAnswer((_) async => [mockCameraInfo]); + when(mockCameraSelector.filter([mockCameraInfo])) + .thenAnswer((_) async => [mockCameraInfo]); + when(mockCameraInfo.getSensorRotationDegrees()) + .thenAnswer((_) async => sensorRotationDegrees); + + // Mock additional ProcessCameraProvider operation that is irrelevant + // for the tests in this file. + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => MockLiveCameraState()); + + return mockProcessCameraProvider; + } + + /// Returns CameraXProxy used to mock all calls to native Android in + /// the `availableCameras` and `createCameraWithSettings` methods. + CameraXProxy getProxyForCreatingTestCamera( + {required MockProcessCameraProvider mockProcessCameraProvider, + required CameraSelector Function(int) createCameraSelector, + required bool handlesCropAndRotation, + required Future Function() getUiOrientation}) => + CameraXProxy( + getProcessCameraProvider: () async => mockProcessCameraProvider, + createCameraSelector: createCameraSelector, + previewSurfaceProducerHandlesCropAndRotation: (_) => + Future.value(handlesCropAndRotation), + getUiOrientation: getUiOrientation, + createPreview: (_, __) => MockPreview(), + createImageCapture: (_, __) => MockImageCapture(), + createRecorder: (_) => MockRecorder(), + createVideoCapture: (_) async => MockVideoCapture(), + createImageAnalysis: (_, __) => MockImageAnalysis(), + createResolutionStrategy: ( + {bool highestAvailable = false, + Size? boundSize, + int? fallbackRule}) => + MockResolutionStrategy(), + createResolutionSelector: (_, __, ___) => MockResolutionSelector(), + createFallbackStrategy: ( + {required VideoQuality quality, + required VideoResolutionFallbackRule fallbackRule}) => + MockFallbackStrategy(), + createQualitySelector: ( + {required VideoQuality videoQuality, + required FallbackStrategy fallbackStrategy}) => + MockQualitySelector(), + createCameraStateObserver: (_) => MockObserver(), + requestCameraPermissions: (_) => Future.value(), + startListeningForDeviceOrientationChange: (_, __) {}, + setPreviewSurfaceProvider: (_) => Future.value( + 3), // 3 is a random Flutter SurfaceTexture ID for testing + createAspectRatioStrategy: (int aspectRatio, int fallbackRule) => + MockAspectRatioStrategy(), + createResolutionFilterWithOnePreferredSize: + (Size preferredResolution) => MockResolutionFilter(), + ); + + /// Returns function that a CameraXProxy can use to select the front camera. + MockCameraSelector Function(int cameraSelectorLensDirection) + createCameraSelectorForFrontCamera( + MockCameraSelector mockCameraSelector) { + return (int cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { + case CameraSelector.lensFacingFront: + return mockCameraSelector; + default: + return MockCameraSelector(); + } + }; + } + + /// Returns function that a CameraXProxy can use to select the back camera. + MockCameraSelector Function(int cameraSelectorLensDirection) + createCameraSelectorForBackCamera(MockCameraSelector mockCameraSelector) { + return (int cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { + case CameraSelector.lensFacingBack: + return mockCameraSelector; + default: + return MockCameraSelector(); + } + }; + } + + /// Error message for detecting an incorrect preview rotation. + String getExpectedRotationTestFailureReason( + int expectedQuarterTurns, int actualQuarterTurns) => + '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. + final MockCameraSelector mockCameraSelector = MockCameraSelector(); + final MockProcessCameraProvider mockProcessCameraProvider = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockCameraSelector, + sensorRotationDegrees: /* irrelevant for test */ 90); + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: (_) => mockCameraSelector, + handlesCropAndRotation: true, + /* irrelevant for test */ getUiOrientation: () => + Future.value(DeviceOrientation.landscapeLeft)); + + // 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 was built. + final Texture texture = tester.widget(find.byType(Texture)); + expect(texture.textureId, cameraId); + + // Verify RotatedBox was not built and thus, the Texture is not rotated. + expect(() => tester.widget(find.byType(RotatedBox)), + throwsStateError); + }); + + group('when handlesCropAndRotation is false,', () { + // Test that preview rotation responds to initial device orientation: + group('sensor orientation degrees is 270, camera is front facing,', () { + late AndroidCameraCameraX camera; + late int cameraId; + late MockCameraSelector mockFrontCameraSelector; + late MockCameraSelector Function(int cameraSelectorLensDirection) + proxyCreateCameraSelectorForFrontCamera; + late MockProcessCameraProvider mockProcessCameraProviderForFrontCamera; + 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. + mockFrontCameraSelector = MockCameraSelector(); + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + mockProcessCameraProviderForFrontCamera = + setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera( + mockCameraSelector: mockFrontCameraSelector, + sensorRotationDegrees: 270); + + // 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, + getUiOrientation: () => + Future.value(DeviceOrientation.portraitUp)); + + // 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. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + // 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)); + }); + + 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: () => Future.value( + DeviceOrientation.landscapeRight)); + + // 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. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + // 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)); + }); + + 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, + getUiOrientation: () => Future.value( + DeviceOrientation.portraitDown)); + + // 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. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + // Verify Texture is rotated by ((270 - 180 * 1 + 360) % 360) - 180 = -90 degrees clockwise = 90 degrees counterclockwise = 270 degrees. + 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 device orientation fixed to DeviceOrientation.landscapeLeft, 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 left. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + handlesCropAndRotation: false, + getUiOrientation: () => Future.value( + DeviceOrientation.landscapeLeft)); + + // 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. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + // 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; + expect(rotatedBox.child, isA()); + expect((rotatedBox.child! as Texture).textureId, cameraId); + expect(clockwiseQuarterTurns, expectedQuarterTurns, + reason: getExpectedRotationTestFailureReason( + expectedQuarterTurns, rotatedBox.quarterTurns)); + }); + }); + + 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; + + // 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(int cameraSelectorLensDirection) + 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. + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + handlesCropAndRotation: false, + getUiOrientation: /* initial device orientation irrelevant for test */ + () => Future.value( + DeviceOrientation.landscapeLeft)); + + // 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 - 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. + await tester.pumpWidget(camera.buildPreview(cameraId)); + + for (final DeviceOrientation currentDeviceOrientation + in expectedRotationPerDeviceOrientation.keys) { + final DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(currentDeviceOrientation); + DeviceOrientationManager.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.'); + } + + await DeviceOrientationManager.deviceOrientationChangedStreamController + .close(); + }); + + // 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(int cameraSelectorLensDirection) + proxyCreateCameraSelectorForBackCamera; + late MediaSettings testMediaSettings; + late DeviceOrientation testInitialDeviceOrientation; + + setUp(() { + camera = AndroidCameraCameraX(); + cameraId = 347; + + // 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); + + testMediaSettings = const MediaSettings(); + }); + + testWidgets( + 'sensor orientation degrees is 90, 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 + // 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: () => + Future.value(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)); + }); + + testWidgets( + 'sensor orientation degrees is 270, 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 + // 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: () => + Future.value(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)); + }); + }); + + // 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', + (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 front camera selection and initial device orientation as landscape right. + final MockCameraSelector Function(int cameraSelectorLensDirection) + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForFrontCamera(mockFrontCameraSelector); + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + handlesCropAndRotation: false, + getUiOrientation: () => + Future.value(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)); + }); + + testWidgets( + 'camera is back 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 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(int cameraSelectorLensDirection) + proxyCreateCameraSelectorForFrontCamera = + createCameraSelectorForBackCamera(mockBackCameraSelector); + camera.proxy = getProxyForCreatingTestCamera( + mockProcessCameraProvider: mockProcessCameraProvider, + createCameraSelector: proxyCreateCameraSelectorForFrontCamera, + handlesCropAndRotation: false, + getUiOrientation: () => + Future.value(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)); + }); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart index b3cbc257b0c..8ba2cee9dc3 100644 --- a/packages/camera/camera_android_camerax/test/preview_test.dart +++ b/packages/camera/camera_android_camerax/test/preview_test.dart @@ -170,5 +170,19 @@ void main() { verify(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview))); }); + + test( + 'surfaceProducerHandlesCropAndRotation makes call to check if Android surface producer automatically corrects camera preview rotation', + () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + final Preview preview = Preview.detached(); + + when(mockApi.surfaceProducerHandlesCropAndRotation()).thenReturn(true); + + expect(await preview.surfaceProducerHandlesCropAndRotation(), true); + + verify(mockApi.surfaceProducerHandlesCropAndRotation()); + }); }); } diff --git a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart index 8976a31245d..d8b7a21e44b 100644 --- a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart @@ -127,6 +127,15 @@ class MockTestPreviewHostApi extends _i1.Mock ), returnValueForMissingStub: null, ); + + @override + bool surfaceProducerHandlesCropAndRotation() => (super.noSuchMethod( + Invocation.method( + #surfaceProducerHandlesCropAndRotation, + [], + ), + returnValue: false, + ) as bool); } /// A class which mocks [ResolutionSelector]. diff --git a/packages/camera/camera_android_camerax/test/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart index d1725515e2f..030f9aee5b2 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.dart @@ -85,16 +85,4 @@ void main() { verify(mockApi.getTempFilePath(testPrefix, testSuffix)); }); }); - - test('isPreviewPreTransformed returns expected answer', () async { - final MockTestSystemServicesHostApi mockApi = - MockTestSystemServicesHostApi(); - TestSystemServicesHostApi.setup(mockApi); - const bool isPreviewPreTransformed = true; - - when(mockApi.isPreviewPreTransformed()).thenReturn(isPreviewPreTransformed); - - expect(await SystemServices.isPreviewPreTransformed(), isTrue); - verify(mockApi.isPreviewPreTransformed()); - }); } diff --git a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart index 9abb64c39bb..ec97625adb9 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart @@ -87,13 +87,4 @@ class MockTestSystemServicesHostApi extends _i1.Mock ), ), ) as String); - - @override - bool isPreviewPreTransformed() => (super.noSuchMethod( - Invocation.method( - #isPreviewPreTransformed, - [], - ), - returnValue: false, - ) as bool); } diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart index 32235b878f3..1007d975ebc 100644 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -510,8 +510,6 @@ abstract class TestSystemServicesHostApi { String getTempFilePath(String prefix, String suffix); - bool isPreviewPreTransformed(); - static void setup(TestSystemServicesHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -563,24 +561,6 @@ abstract class TestSystemServicesHostApi { }); } } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesHostApi.isPreviewPreTransformed', - codec, - binaryMessenger: binaryMessenger); - if (api == null) { - _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); - } else { - _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, - (Object? message) async { - // ignore message - final bool output = api.isPreviewPreTransformed(); - return [output]; - }); - } - } } } @@ -722,6 +702,8 @@ abstract class TestPreviewHostApi { void setTargetRotation(int identifier, int rotation); + bool surfaceProducerHandlesCropAndRotation(); + static void setup(TestPreviewHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -835,6 +817,24 @@ abstract class TestPreviewHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.surfaceProducerHandlesCropAndRotation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + // ignore message + final bool output = api.surfaceProducerHandlesCropAndRotation(); + return [output]; + }); + } + } } }