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];
+ });
+ }
+ }
}
}