diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 8307d7077d8a..253c2f11a29b 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.6.6 + +* Adds logic to support building a camera preview with Android `Surface`s not backed by a `SurfaceTexture` + to which CameraX cannot not automatically apply the transformation required to achieve the correct rotation. +* Adds fix for incorrect camera preview rotation on naturally landscape-oriented devices. +* Updates example app's minimum supported SDK version to Flutter 3.22/Dart 3.4. + ## 0.6.5+6 * Updates Guava version to 33.2.1. diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoHostApiImpl.java index 43fd0a383b00..76606b43efc0 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoHostApiImpl.java @@ -29,26 +29,30 @@ public class Camera2CameraInfoHostApiImpl implements Camera2CameraInfoHostApi { /** Proxy for methods of {@link Camera2CameraInfo}. */ @VisibleForTesting + @OptIn(markerClass = ExperimentalCamera2Interop.class) public static class Camera2CameraInfoProxy { @NonNull - @OptIn(markerClass = ExperimentalCamera2Interop.class) public Camera2CameraInfo createFrom(@NonNull CameraInfo cameraInfo) { return Camera2CameraInfo.from(cameraInfo); } @NonNull - @OptIn(markerClass = ExperimentalCamera2Interop.class) public Integer getSupportedHardwareLevel(@NonNull Camera2CameraInfo camera2CameraInfo) { return camera2CameraInfo.getCameraCharacteristic( CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); } @NonNull - @OptIn(markerClass = ExperimentalCamera2Interop.class) public String getCameraId(@NonNull Camera2CameraInfo camera2CameraInfo) { return camera2CameraInfo.getCameraId(); } + + @NonNull + public Long getSensorOrientation(@NonNull Camera2CameraInfo camera2CameraInfo) { + return Long.valueOf( + camera2CameraInfo.getCameraCharacteristic(CameraCharacteristics.SENSOR_ORIENTATION)); + } } /** @@ -105,6 +109,12 @@ public String getCameraId(@NonNull Long identifier) { return proxy.getCameraId(getCamera2CameraInfoInstance(identifier)); } + @Override + @NonNull + public Long getSensorOrientation(@NonNull Long identifier) { + return proxy.getSensorOrientation(getCamera2CameraInfoInstance(identifier)); + } + private Camera2CameraInfo getCamera2CameraInfoInstance(@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/DeviceOrientationManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java index b5281179d728..a8ad257a2bc4 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 @@ -52,8 +52,7 @@ interface DeviceOrientationChangeCallback { * Starts listening to the device's sensors or UI for orientation updates. * *

When orientation information is updated, the callback method of the {@link - * DeviceOrientationChangeCallback} is called with the new orientation. This latest value can also - * be retrieved through the {@link #getVideoOrientation()} accessor. + * DeviceOrientationChangeCallback} is called with the new orientation. * *

If the device's ACCELEROMETER_ROTATION setting is enabled the {@link * DeviceOrientationManager} will report orientation updates based on the sensor information. If @@ -124,7 +123,7 @@ static void handleOrientationChange( */ // Configuration.ORIENTATION_SQUARE is deprecated. @SuppressWarnings("deprecation") - @VisibleForTesting + @NonNull PlatformChannel.DeviceOrientation getUIOrientation() { final int rotation = getDefaultRotation(); final int orientation = activity.getResources().getConfiguration().orientation; diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java index 22bba48e1bad..363bc39b2780 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java @@ -95,7 +95,8 @@ public void stopListeningForDeviceOrientationChange() { * for instance for more information on how this default value is used. */ @Override - public @NonNull Long getDefaultDisplayRotation() { + @NonNull + public Long getDefaultDisplayRotation() { int defaultRotation; try { defaultRotation = deviceOrientationManager.getDefaultRotation(); @@ -106,4 +107,11 @@ public void stopListeningForDeviceOrientationChange() { return Long.valueOf(defaultRotation); } + + /** Gets current UI orientation based on the current device orientation and rotation. */ + @Override + @NonNull + public String getUiOrientation() { + return serializeDeviceOrientation(deviceOrientationManager.getUIOrientation()); + } } 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 cbfc36f35cbd..7adf02595ad9 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,6 +1441,9 @@ 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; @@ -1508,6 +1511,29 @@ 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. */ @@ -1550,6 +1576,9 @@ void startListeningForDeviceOrientationChange( @NonNull Long getDefaultDisplayRotation(); + @NonNull + String getUiOrientation(); + /** The codec used by DeviceOrientationManagerHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -1634,6 +1663,29 @@ static void setup( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.DeviceOrientationManagerHostApi.getUiOrientation", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + String output = api.getUiOrientation(); + 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. */ @@ -4389,6 +4441,9 @@ public interface Camera2CameraInfoHostApi { @NonNull String getCameraId(@NonNull Long identifier); + @NonNull + Long getSensorOrientation(@NonNull Long identifier); + /** The codec used by Camera2CameraInfoHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -4481,6 +4536,33 @@ static void setup( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.Camera2CameraInfoHostApi.getSensorOrientation", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getSensorOrientation( + (identifierArg == null) ? null : identifierArg.longValue()); + 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. */ 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 d058d62fe224..cd048238ca9c 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,6 +6,7 @@ import android.app.Activity; import android.content.Context; +import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -103,4 +104,18 @@ public String getTempFilePath(@NonNull String prefix, @NonNull String suffix) { null); } } + + /** + * Returns whether or not a {@code SurfaceTexture} backs the {@code Surface} provided to CameraX + * to build the camera preview. If it is backed by a {@code Surface}, then the transformation + * needed to correctly rotate the preview has already been applied. + * + *

This is determined by the engine, who uses {@code SurfaceTexture}s on Android SDKs 29 and + * below. + */ + @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/DeviceOrientationManagerWrapperTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerWrapperTest.java index 26cfd77f1265..ba7335971f1d 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerWrapperTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerWrapperTest.java @@ -93,4 +93,16 @@ public void getDefaultDisplayRotation_returnsExpectedRotation() { assertEquals(hostApi.getDefaultDisplayRotation(), Long.valueOf(defaultRotation)); } + + @Test + public void getUiOrientation_returnsExpectedOrientation() { + final DeviceOrientationManagerHostApiImpl hostApi = + new DeviceOrientationManagerHostApiImpl(mockBinaryMessenger, mockInstanceManager); + final DeviceOrientation uiOrientation = DeviceOrientation.LANDSCAPE_LEFT; + + hostApi.deviceOrientationManager = mockDeviceOrientationManager; + when(mockDeviceOrientationManager.getUIOrientation()).thenReturn(uiOrientation); + + assertEquals(hostApi.getUiOrientation(), uiOrientation.toString()); + } } 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 3636629e75f7..252d3a0a776e 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,7 +5,9 @@ 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; @@ -24,12 +26,16 @@ import java.io.IOException; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockedStatic; 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 { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -130,4 +136,28 @@ 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 = 29) + public void isPreviewPreTransformed_returnsTrueWhenRunningSdk29() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); + assertTrue(systemServicesHostApi.isPreviewPreTransformed()); + } + + @Test + @Config(sdk = 30) + public void isPreviewPreTransformed_returnsFalseWhenRunningAboveSdk29() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); + assertFalse(systemServicesHostApi.isPreviewPreTransformed()); + } } diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml index f16b8d5433b1..c47771b1a12a 100644 --- a/packages/camera/camera_android_camerax/example/pubspec.yaml +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the camera_android_camerax plugin. publish_to: 'none' environment: - sdk: ^3.2.0 - flutter: ">=3.16.0" + sdk: ^3.4.0 + flutter: ">=3.22.0" dependencies: camera_android_camerax: 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 edf24bab37a0..eec9ee8b7762 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 @@ -10,7 +10,7 @@ import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/services.dart' show DeviceOrientation, PlatformException; import 'package:flutter/widgets.dart' - show Size, Texture, Widget, visibleForTesting; + show RotatedBox, Size, Texture, Widget, visibleForTesting; import 'package:stream_transform/stream_transform.dart'; import 'analyzer.dart'; @@ -233,6 +233,34 @@ class AndroidCameraCameraX extends CameraPlatform { static const String exposureCompensationNotSupported = 'exposureCompensationNotSupported'; + /// Whether or not the created camera is front facing. + @visibleForTesting + late bool cameraIsFrontFacing; + + /// Whether or not the Surface used to create the camera preview is backed + /// by a SurfaceTexture. + @visibleForTesting + late bool isPreviewPreTransformed; + + /// The initial orientation of the device. + /// + /// The camera preview will use this orientation as the natural orientation + /// to correct its rotation with respect to, if necessary. + @visibleForTesting + DeviceOrientation? naturalOrientation; + + /// The camera sensor orientation. + @visibleForTesting + late int sensorOrientation; + + /// The current orientation of the device. + @visibleForTesting + DeviceOrientation? currentDeviceOrientation; + + /// Subscription for listening to changes in device orientation. + StreamSubscription? + _subscriptionForDeviceOrientationChanges; + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { @@ -321,7 +349,7 @@ class AndroidCameraCameraX extends CameraPlatform { // Save CameraSelector that matches cameraDescription. final int cameraSelectorLensDirection = _getCameraSelectorLensDirection(cameraDescription.lensDirection); - final bool cameraIsFrontFacing = + cameraIsFrontFacing = cameraSelectorLensDirection == CameraSelector.lensFacingFront; cameraSelector = proxy.createCameraSelector(cameraSelectorLensDirection); // Start listening for device orientation changes preceding camera creation. @@ -366,6 +394,26 @@ class AndroidCameraCameraX extends CameraPlatform { previewInitiallyBound = true; _previewIsPaused = false; + // Retrieve info required for correcting the rotation of the camera preview + // if necessary. + + final Camera2CameraInfo camera2CameraInfo = + await proxy.getCamera2CameraInfo(cameraInfo!); + await Future.wait(>[ + SystemServices.isPreviewPreTransformed() + .then((bool value) => isPreviewPreTransformed = value), + proxy + .getSensorOrientation(camera2CameraInfo) + .then((int value) => sensorOrientation = value), + proxy + .getUiOrientation() + .then((DeviceOrientation value) => naturalOrientation ??= value), + ]); + _subscriptionForDeviceOrientationChanges = onDeviceOrientationChanged() + .listen((DeviceOrientationChangedEvent event) { + currentDeviceOrientation = event.orientation; + }); + return flutterSurfaceTextureId; } @@ -425,6 +473,7 @@ class AndroidCameraCameraX extends CameraPlatform { await liveCameraState?.removeObservers(); processCameraProvider?.unbindAll(); await imageAnalysis?.clearAnalyzer(); + await _subscriptionForDeviceOrientationChanges?.cancel(); } /// The camera has been initialized. @@ -815,7 +864,69 @@ class AndroidCameraCameraX extends CameraPlatform { "Camera not found. Please call the 'create' method before calling 'buildPreview'", ); } - return Texture(textureId: cameraId); + + final Widget cameraPreview = Texture(textureId: cameraId); + final Map degreesForDeviceOrientation = + { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeRight: 90, + DeviceOrientation.portraitDown: 180, + DeviceOrientation.landscapeLeft: 270, + }; + int naturalDeviceOrientationDegrees = + degreesForDeviceOrientation[naturalOrientation]!; + + if (isPreviewPreTransformed) { + // If the camera preview is backed by a SurfaceTexture, the transformation + // needed to correctly rotate the preview has already been applied. + // However, we may need to correct the camera preview rotation if the + // device is naturally landscape-oriented. + if (naturalOrientation == DeviceOrientation.landscapeLeft || + naturalOrientation == DeviceOrientation.landscapeRight) { + final int quarterTurnsToCorrectForLandscape = + (-naturalDeviceOrientationDegrees + 360) ~/ 4; + return RotatedBox( + quarterTurns: quarterTurnsToCorrectForLandscape, + child: cameraPreview); + } + return cameraPreview; + } + + // Fix for the rotation of the camera preview not backed by a SurfaceTexture + // with respect to the naturalOrientation of the device: + + final int signForCameraDirection = cameraIsFrontFacing ? 1 : -1; + + if (signForCameraDirection == 1 && + (currentDeviceOrientation == DeviceOrientation.landscapeLeft || + currentDeviceOrientation == DeviceOrientation.landscapeRight)) { + // For front-facing cameras, the image buffer is rotated counterclockwise, + // so we determine the rotation needed to correct the camera preview with + // respect to the naturalOrientation of the device based on the inverse of + // naturalOrientation. + naturalDeviceOrientationDegrees += 180; + } + + // See https://developer.android.com/media/camera/camera2/camera-preview#orientation_calculation + // for more context on this formula. + final double rotation = (sensorOrientation + + naturalDeviceOrientationDegrees * signForCameraDirection + + 360) % + 360; + int quarterTurnsToCorrectPreview = rotation ~/ 90; + + if (naturalOrientation == DeviceOrientation.landscapeLeft || + naturalOrientation == DeviceOrientation.landscapeRight) { + // We may need to correct the camera preview rotation if the device is + // naturally landscape-oriented. + quarterTurnsToCorrectPreview += + (-naturalDeviceOrientationDegrees + 360) ~/ 4; + return RotatedBox( + quarterTurns: quarterTurnsToCorrectPreview, child: cameraPreview); + } + + return RotatedBox( + quarterTurns: quarterTurnsToCorrectPreview, child: cameraPreview); } /// Captures an image and returns the file where it was saved. diff --git a/packages/camera/camera_android_camerax/lib/src/camera2_camera_info.dart b/packages/camera/camera_android_camerax/lib/src/camera2_camera_info.dart index fafb90f0ecb5..88b44238f3b9 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera2_camera_info.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera2_camera_info.dart @@ -53,6 +53,10 @@ class Camera2CameraInfo extends JavaObject { /// The ID may change based on the internal configuration of the camera to which /// this instances pertains. Future getCameraId() => _api.getCameraIdFromInstance(this); + + /// Retrieves the orientation of the camera sensor. + Future getSensorOrientation() => + _api.getSensorOrientationFromInstance(this); } /// Host API implementation of [Camera2CameraInfo]. @@ -91,6 +95,11 @@ class _Camera2CameraInfoHostApiImpl extends Camera2CameraInfoHostApi { final int? identifier = instanceManager.getIdentifier(instance); return getCameraId(identifier!); } + + Future getSensorOrientationFromInstance(Camera2CameraInfo instance) { + final int? identifier = instanceManager.getIdentifier(instance); + return getSensorOrientation(identifier!); + } } /// Flutter API Implementation of [Camera2CameraInfo]. 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 e63a7a6afaf9..a9461eaaae03 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,6 +1003,33 @@ 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 { @@ -1117,6 +1144,33 @@ class DeviceOrientationManagerHostApi { return (replyList[0] as int?)!; } } + + Future getUiOrientation() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DeviceOrientationManagerHostApi.getUiOrientation', + 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 String?)!; + } + } } abstract class DeviceOrientationManagerFlutterApi { @@ -3575,6 +3629,34 @@ class Camera2CameraInfoHostApi { return (replyList[0] as String?)!; } } + + Future getSensorOrientation(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.Camera2CameraInfoHostApi.getSensorOrientation', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) 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 int?)!; + } + } } abstract class Camera2CameraInfoFlutterApi { 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 d81d1c761c6a..feae868ac40c 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart @@ -4,6 +4,8 @@ import 'dart:ui' show Size; +import 'package:flutter/services.dart' show DeviceOrientation; + import 'analyzer.dart'; import 'aspect_ratio_strategy.dart'; import 'camera2_camera_control.dart'; @@ -66,6 +68,8 @@ class CameraXProxy { this.createResolutionFilterWithOnePreferredSize = _createAttachedResolutionFilterWithOnePreferredSize, this.getCamera2CameraInfo = _getCamera2CameraInfo, + this.getUiOrientation = _getUiOrientation, + this.getSensorOrientation = _getSensorOrientation, }); /// Returns a [ProcessCameraProvider] instance. @@ -189,6 +193,13 @@ class CameraXProxy { Future Function(CameraInfo cameraInfo) getCamera2CameraInfo; + /// Gets current UI orientation based on device orientation and rotation. + Future Function() getUiOrientation; + + /// Gets camera sensor orientation from [camera2CameraInfo]. + Future Function(Camera2CameraInfo camera2CameraInfo) + getSensorOrientation; + static Future _getProcessCameraProvider() { return ProcessCameraProvider.getInstance(); } @@ -335,4 +346,13 @@ class CameraXProxy { CameraInfo cameraInfo) async { return Camera2CameraInfo.from(cameraInfo); } + + static Future _getUiOrientation() async { + return DeviceOrientationManager.getUiOrientation(); + } + + static Future _getSensorOrientation( + Camera2CameraInfo camera2CameraInfo) async { + return camera2CameraInfo.getSensorOrientation(); + } } diff --git a/packages/camera/camera_android_camerax/lib/src/device_orientation_manager.dart b/packages/camera/camera_android_camerax/lib/src/device_orientation_manager.dart index 10f20232485b..aacddc5788a0 100644 --- a/packages/camera/camera_android_camerax/lib/src/device_orientation_manager.dart +++ b/packages/camera/camera_android_camerax/lib/src/device_orientation_manager.dart @@ -67,6 +67,16 @@ class DeviceOrientationManager { return api.getDefaultDisplayRotation(); } + /// Retrieves the current UI orientation based on the current device + /// orientation and screen rotation. + static Future getUiOrientation( + {BinaryMessenger? binaryMessenger}) async { + final DeviceOrientationManagerHostApi api = + DeviceOrientationManagerHostApi(binaryMessenger: binaryMessenger); + + return deserializeDeviceOrientation(await api.getUiOrientation()); + } + /// Serializes [DeviceOrientation] into a [String]. static String serializeDeviceOrientation(DeviceOrientation orientation) { switch (orientation) { @@ -80,6 +90,24 @@ class DeviceOrientationManager { return 'PORTRAIT_UP'; } } + + /// Deserializes device orientation in [String] format into a + /// [DeviceOrientation]. + static DeviceOrientation deserializeDeviceOrientation(String orientation) { + switch (orientation) { + case 'LANDSCAPE_LEFT': + return DeviceOrientation.landscapeLeft; + case 'LANDSCAPE_RIGHT': + return DeviceOrientation.landscapeRight; + case 'PORTRAIT_DOWN': + return DeviceOrientation.portraitDown; + case 'PORTRAIT_UP': + return DeviceOrientation.portraitUp; + default: + throw ArgumentError( + '"$orientation" is not a valid DeviceOrientation value'); + } + } } /// Flutter API implementation of [DeviceOrientationManager]. @@ -96,26 +124,8 @@ class DeviceOrientationManagerFlutterApiImpl @override void onDeviceOrientationChanged(String orientation) { final DeviceOrientation deviceOrientation = - deserializeDeviceOrientation(orientation); + DeviceOrientationManager.deserializeDeviceOrientation(orientation); DeviceOrientationManager.deviceOrientationChangedStreamController .add(DeviceOrientationChangedEvent(deviceOrientation)); } - - /// Deserializes device orientation in [String] format into a - /// [DeviceOrientation]. - DeviceOrientation deserializeDeviceOrientation(String orientation) { - switch (orientation) { - case 'LANDSCAPE_LEFT': - return DeviceOrientation.landscapeLeft; - case 'LANDSCAPE_RIGHT': - return DeviceOrientation.landscapeRight; - case 'PORTRAIT_DOWN': - return DeviceOrientation.portraitDown; - case 'PORTRAIT_UP': - return DeviceOrientation.portraitUp; - default: - throw ArgumentError( - '"$orientation" is not a valid DeviceOrientation value'); - } - } } 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 b75a1cb98035..a55981213266 100644 --- a/packages/camera/camera_android_camerax/lib/src/system_services.dart +++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart @@ -48,6 +48,22 @@ 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}) { + // TODO(camsim99): Make call to Java host to determine true value when + // Impeller support is re-landed. + // https://github.com/flutter/flutter/issues/149294 + return Future.value(true); + } } /// 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 872c3a622390..949830db3e83 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -260,6 +260,8 @@ abstract class SystemServicesHostApi { CameraPermissionsErrorData? requestCameraPermissions(bool enableAudio); String getTempFilePath(String prefix, String suffix); + + bool isPreviewPreTransformed(); } @FlutterApi() @@ -275,6 +277,8 @@ abstract class DeviceOrientationManagerHostApi { void stopListeningForDeviceOrientationChange(); int getDefaultDisplayRotation(); + + String getUiOrientation(); } @FlutterApi() @@ -562,6 +566,8 @@ abstract class Camera2CameraInfoHostApi { int getSupportedHardwareLevel(int identifier); String getCameraId(int identifier); + + int getSensorOrientation(int identifier); } @FlutterApi() diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index eb94986cbe9e..e4c534e53f94 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.5+6 +version: 0.6.6 environment: sdk: ^3.4.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 aa33254ac14a..1f9ea762ce23 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,8 @@ 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, RotatedBox, Size, Texture, Widget; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -197,6 +198,10 @@ void main() { (Size preferredResolution) => ResolutionFilter.onePreferredSizeDetached( preferredResolution: preferredResolution), + getCamera2CameraInfo: (_) => + Future.value(MockCamera2CameraInfo()), + getUiOrientation: () => + Future.value(DeviceOrientation.portraitUp), ); /// CameraXProxy for testing exposure and focus related controls. @@ -338,6 +343,10 @@ void main() { final MockCamera mockCamera = MockCamera(); final MockCameraInfo mockCameraInfo = MockCameraInfo(); final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); + bool cameraPermissionsRequested = false; bool startedListeningForDeviceOrientationChanges = false; @@ -385,6 +394,10 @@ void main() { }, createAspectRatioStrategy: (_, __) => MockAspectRatioStrategy(), createResolutionFilterWithOnePreferredSize: (_) => MockResolutionFilter(), + getCamera2CameraInfo: (_) => + Future.value(MockCamera2CameraInfo()), + getUiOrientation: () => + Future.value(DeviceOrientation.portraitUp), ); camera.processCameraProvider = mockProcessCameraProvider; @@ -467,6 +480,10 @@ void main() { final MockCamera mockCamera = MockCamera(); final MockCameraInfo mockCameraInfo = MockCameraInfo(); final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); // Tell plugin to create mock/detached objects and stub method calls for the // testing of createCamera. @@ -507,6 +524,12 @@ void main() { startListeningForDeviceOrientationChange: (_, __) {}, createAspectRatioStrategy: (_, __) => MockAspectRatioStrategy(), createResolutionFilterWithOnePreferredSize: (_) => MockResolutionFilter(), + getCamera2CameraInfo: (CameraInfo cameraInfo) => + cameraInfo == mockCameraInfo + ? Future.value(mockCamera2CameraInfo) + : Future.value(MockCamera2CameraInfo()), + getUiOrientation: () => + Future.value(DeviceOrientation.portraitUp), ); when(mockProcessCameraProvider.bindToLifecycle(mockBackCameraSelector, @@ -517,6 +540,7 @@ void main() { .thenAnswer((_) async => MockLiveCameraState()); when(mockCamera.getCameraControl()) .thenAnswer((_) async => mockCameraControl); + camera.processCameraProvider = mockProcessCameraProvider; await camera.createCameraWithSettings( @@ -562,6 +586,9 @@ void main() { final MockProcessCameraProvider mockProcessCameraProvider = MockProcessCameraProvider(); final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); // Tell plugin to create mock/detached objects for testing createCamera // as needed. @@ -893,6 +920,65 @@ void main() { expect(camera.recorder!.qualitySelector, isNull); }); + test( + 'createCamera sets sensor and device orientations needed to correct preview rotation as expected', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 270; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation); + const bool enableAudio = true; + const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const DeviceOrientation testUiOrientation = DeviceOrientation.portraitDown; + const DeviceOrientation testCurrentOrientation = + DeviceOrientation.portraitUp; + + // Mock/Detached objects for (typically attached) objects created by + // createCamera. + final MockCamera mockCamera = MockCamera(); + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); + + // The proxy needed for this test is the same as testing resolution + // presets except for mocking the retrievall of the sensor and current + // UI orientation. + camera.proxy = + getProxyForTestingResolutionPreset(mockProcessCameraProvider); + camera.proxy.getSensorOrientation = + (_) async => Future.value(testSensorOrientation); + camera.proxy.getUiOrientation = + () async => Future.value(testUiOrientation); + + when(mockProcessCameraProvider.bindToLifecycle(any, any)) + .thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when(mockCameraInfo.getCameraState()) + .thenAnswer((_) async => MockLiveCameraState()); + + await camera.createCamera(testCameraDescription, testResolutionPreset, + enableAudio: enableAudio); + + const DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(testCurrentOrientation); + + DeviceOrientationManager.deviceOrientationChangedStreamController + .add(testEvent); + + // Wait for currentDeviceOrientation to update. + await Future.value(); + + expect(camera.naturalOrientation, testUiOrientation); + expect(camera.sensorOrientation, testSensorOrientation); + expect(camera.currentDeviceOrientation, testCurrentOrientation); + }); + test( 'initializeCamera throws a CameraException when createCamera has not been called before initializedCamera', () async { @@ -927,6 +1013,9 @@ void main() { final MockPreview mockPreview = MockPreview(); final MockImageCapture mockImageCapture = MockImageCapture(); final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); // Tell plugin to create mock/detached objects for testing createCamera // as needed. @@ -967,6 +1056,10 @@ void main() { startListeningForDeviceOrientationChange: (_, __) {}, createAspectRatioStrategy: (_, __) => MockAspectRatioStrategy(), createResolutionFilterWithOnePreferredSize: (_) => MockResolutionFilter(), + getCamera2CameraInfo: (_) => + Future.value(MockCamera2CameraInfo()), + getUiOrientation: () => + Future.value(DeviceOrientation.portraitUp), ); final CameraInitializedEvent testCameraInitializedEvent = @@ -1234,7 +1327,7 @@ void main() { }); test( - 'buildPreview returns a Texture once the preview is bound to the lifecycle', + '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; @@ -1243,12 +1336,148 @@ void main() { // bound to the lifecycle of the camera. camera.previewInitiallyBound = true; + // Tell camera the Surface used to build camera preview is backed by a + // SurfaceTexture. + camera.isPreviewPreTransformed = true; + + camera.naturalOrientation = DeviceOrientation.portraitDown; + final Widget widget = camera.buildPreview(cameraId); expect(widget is Texture, isTrue); expect((widget as Texture).textureId, cameraId); }); + test( + 'buildPreview returns preview with expected rotation if camera is not front facing and the preview is not backed by a SurfaceTexture', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 33; + + // Tell camera that createCamera has been called and thus, preview has been + // bound to the lifecycle of the camera. + camera.previewInitiallyBound = true; + + // Tell camera the Surface used to build camera preview is not backed by a + // SurfaceTexture. + camera.isPreviewPreTransformed = false; + + // Mock sensor and device orientation. + camera.sensorOrientation = 270; + camera.cameraIsFrontFacing = false; + camera.naturalOrientation = DeviceOrientation.portraitUp; + + final double expectedRotation = (camera.sensorOrientation + + 0 /* the natural orientation in clockwise degrees */ * + -1 /* camera is not front facing */ + + 360) % + 360; + final int expectedQuarterTurns = (expectedRotation / 90).toInt(); + + final Widget widget = camera.buildPreview(cameraId); + + expect(widget is RotatedBox, isTrue); + expect((widget as RotatedBox).quarterTurns, expectedQuarterTurns); + expect(widget.child is Texture, isTrue); + expect((widget.child! as Texture).textureId, cameraId); + }); + + test( + 'buildPreview returns preview with expected rotation if camera is front facing, the current orientation is landscape, and the preview is not backed by a SurfaceTexture', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 7; + + // Tell camera that createCamera has been called and thus, preview has been + // bound to the lifecycle of the camera. + camera.previewInitiallyBound = true; + + // Tell camera the Surface used to build camera preview is not backed by a + // SurfaceTexture. + camera.isPreviewPreTransformed = false; + + // Mock sensor and device orientation. + camera.sensorOrientation = 270; + camera.naturalOrientation = DeviceOrientation.portraitUp; + camera.cameraIsFrontFacing = true; + + // Calculate expected rotation with offset due to counter-clockwise rotation + // of the image with th efront camera in use. + final double expectedRotation = ((camera.sensorOrientation + + 0 /* the natural orientation in clockwise degrees */ * + 1 /* camera is front facing */ + + 360) % + 360) + + 180; + final int expectedQuarterTurns = (expectedRotation / 90).toInt() % 4; + + // Test landscape left. + camera.currentDeviceOrientation = DeviceOrientation.landscapeLeft; + Widget widget = camera.buildPreview(cameraId); + + expect(widget is RotatedBox, isTrue); + expect((widget as RotatedBox).quarterTurns, expectedQuarterTurns); + expect(widget.child is Texture, isTrue); + expect((widget.child! as Texture).textureId, cameraId); + + // Test landscape right. + camera.currentDeviceOrientation = DeviceOrientation.landscapeRight; + widget = camera.buildPreview(cameraId); + + expect(widget is RotatedBox, isTrue); + expect((widget as RotatedBox).quarterTurns, expectedQuarterTurns); + expect(widget.child is Texture, isTrue); + expect((widget.child! as Texture).textureId, cameraId); + }); + + test( + 'buildPreview returns preview with expected rotation if camera is front facing, the current orientation is not landscape, and the preview is not backed by a SurfaceTexture', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 733; + + // Tell camera that createCamera has been called and thus, preview has been + // bound to the lifecycle of the camera. + camera.previewInitiallyBound = true; + + // Tell camera the Surface used to build camera preview is not backed by a + // SurfaceTexture. + camera.isPreviewPreTransformed = false; + + // Mock sensor and device orientation. + camera.sensorOrientation = 270; + camera.naturalOrientation = DeviceOrientation.portraitUp; + camera.cameraIsFrontFacing = true; + + // Calculate expected rotation without offset needed for landscape orientations + // due to counter-clockwise rotation of the image with th efront camera in use. + final double expectedRotation = (camera.sensorOrientation + + 0 /* the natural orientation in clockwise degrees */ * + 1 /* camera is front facing */ + + 360) % + 360; + + final int expectedQuarterTurns = (expectedRotation / 90).toInt() % 4; + + // Test portrait up. + camera.currentDeviceOrientation = DeviceOrientation.portraitUp; + Widget widget = camera.buildPreview(cameraId); + + expect(widget is RotatedBox, isTrue); + expect((widget as RotatedBox).quarterTurns, expectedQuarterTurns); + expect(widget.child is Texture, isTrue); + expect((widget.child! as Texture).textureId, cameraId); + + // Test portrait down. + camera.currentDeviceOrientation = DeviceOrientation.portraitDown; + widget = camera.buildPreview(cameraId); + + expect(widget is RotatedBox, isTrue); + expect((widget as RotatedBox).quarterTurns, expectedQuarterTurns); + expect(widget.child is Texture, isTrue); + expect((widget.child! 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 0bdfc06af92b..316b12759692 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 @@ -729,6 +729,16 @@ class MockCamera2CameraInfo extends _i1.Mock implements _i26.Camera2CameraInfo { ), )), ) as _i17.Future); + + @override + _i17.Future getSensorOrientation() => (super.noSuchMethod( + Invocation.method( + #getSensorOrientation, + [], + ), + returnValue: _i17.Future.value(0), + returnValueForMissingStub: _i17.Future.value(0), + ) as _i17.Future); } /// A class which mocks [CameraImageData]. @@ -1400,6 +1410,16 @@ 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/camera2_camera_info_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera2_camera_info_test.mocks.dart index 9060c51874ac..88b219f28cf1 100644 --- a/packages/camera/camera_android_camerax/test/camera2_camera_info_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera2_camera_info_test.mocks.dart @@ -90,6 +90,15 @@ class MockTestCamera2CameraInfoHostApi extends _i1.Mock ), ), ) as String); + + @override + int getSensorOrientation(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getSensorOrientation, + [identifier], + ), + returnValue: 0, + ) as int); } /// A class which mocks [TestInstanceManagerHostApi]. diff --git a/packages/camera/camera_android_camerax/test/device_orientation_manager_test.dart b/packages/camera/camera_android_camerax/test/device_orientation_manager_test.dart index de24bb0b3f45..2555302f4092 100644 --- a/packages/camera/camera_android_camerax/test/device_orientation_manager_test.dart +++ b/packages/camera/camera_android_camerax/test/device_orientation_manager_test.dart @@ -61,6 +61,20 @@ void main() { verify(mockApi.getDefaultDisplayRotation()); }); + test('getUiOrientation returns expected orientation', () async { + final MockTestDeviceOrientationManagerHostApi mockApi = + MockTestDeviceOrientationManagerHostApi(); + TestDeviceOrientationManagerHostApi.setup(mockApi); + const DeviceOrientation expectedOrientation = + DeviceOrientation.landscapeRight; + + when(mockApi.getUiOrientation()).thenReturn('LANDSCAPE_RIGHT'); + + expect(await DeviceOrientationManager.getUiOrientation(), + equals(expectedOrientation)); + verify(mockApi.getUiOrientation()); + }); + test('onDeviceOrientationChanged adds new orientation to stream', () { DeviceOrientationManager.deviceOrientationChangedStreamController.stream .listen((DeviceOrientationChangedEvent event) { diff --git a/packages/camera/camera_android_camerax/test/device_orientation_manager_test.mocks.dart b/packages/camera/camera_android_camerax/test/device_orientation_manager_test.mocks.dart index 6f74ad42958d..9d166bc39e19 100644 --- a/packages/camera/camera_android_camerax/test/device_orientation_manager_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/device_orientation_manager_test.mocks.dart @@ -4,6 +4,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i3; import 'test_camerax_library.g.dart' as _i2; @@ -81,4 +82,19 @@ class MockTestDeviceOrientationManagerHostApi extends _i1.Mock ), returnValue: 0, ) as int); + + @override + String getUiOrientation() => (super.noSuchMethod( + Invocation.method( + #getUiOrientation, + [], + ), + returnValue: _i3.dummyValue( + this, + Invocation.method( + #getUiOrientation, + [], + ), + ), + ) as String); } 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 030f9aee5b26..1d5d00ee7169 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.dart @@ -85,4 +85,8 @@ void main() { verify(mockApi.getTempFilePath(testPrefix, testSuffix)); }); }); + + test('isPreviewPreTransformed returns expected answer', () async { + expect(await SystemServices.isPreviewPreTransformed(), isTrue); + }); } 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 ec97625adb94..9abb64c39bb4 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,4 +87,13 @@ 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 105447e52689..32235b878f39 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,6 +510,8 @@ abstract class TestSystemServicesHostApi { String getTempFilePath(String prefix, String suffix); + bool isPreviewPreTransformed(); + static void setup(TestSystemServicesHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -561,6 +563,24 @@ 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]; + }); + } + } } } @@ -576,6 +596,8 @@ abstract class TestDeviceOrientationManagerHostApi { int getDefaultDisplayRotation(); + String getUiOrientation(); + static void setup(TestDeviceOrientationManagerHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -641,6 +663,24 @@ abstract class TestDeviceOrientationManagerHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DeviceOrientationManagerHostApi.getUiOrientation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + // ignore message + final String output = api.getUiOrientation(); + return [output]; + }); + } + } } } @@ -2445,6 +2485,8 @@ abstract class TestCamera2CameraInfoHostApi { String getCameraId(int identifier); + int getSensorOrientation(int identifier); + static void setup(TestCamera2CameraInfoHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2514,5 +2556,28 @@ abstract class TestCamera2CameraInfoHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.Camera2CameraInfoHostApi.getSensorOrientation', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.Camera2CameraInfoHostApi.getSensorOrientation was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.Camera2CameraInfoHostApi.getSensorOrientation was null, expected non-null int.'); + final int output = api.getSensorOrientation(arg_identifier!); + return [output]; + }); + } + } } }