[camera_android_camerax] Fix 90°-off preview rotation (#8629)

Fixes 90°-off preview rotation without locked capture orientation. Part of https://github.com/flutter/flutter/issues/154241.

The major changes in this PR:

1. Ensures the preview widget is rebuilt when a new device orientation is detected by changing the `buildPreview` to return a `StatefulWidget` instead of a `Texture` that wouldn't pick up changes in device orientation.
2. Uses [`handlesCropAndRotation`](https://api.flutter.dev/javadoc/io/flutter/view/TextureRegistry.SurfaceProducer.html#handlesCropAndRotation()) to detect if the preview is already correctly rotated or not. This API was added exactly for that purpose.
3. Corrects the preview when needed using https://developer.android.com/media/camera/camera2/camera-preview#orientation_calculation (also subtracts rotation applied in `CameraPreview` widget).

See https://github.com/flutter/flutter/issues/154241#issuecomment-2655098620 for slightly more context if desired.
diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart
index bcec28f..73359e4 100644
--- a/packages/camera/camera/test/camera_preview_test.dart
+++ b/packages/camera/camera/test/camera_preview_test.dart
@@ -140,7 +140,7 @@
 void main() {
   group('RotatedBox (Android only)', () {
     testWidgets(
-        'when recording rotatedBox should turn according to recording orientation',
+        'when recording in DeviceOrientaiton.portraitUp, rotatedBox should not be rotated',
         (
       WidgetTester tester,
     ) async {
@@ -148,17 +148,18 @@
 
       final FakeController controller = FakeController();
       addTearDown(controller.dispose);
+
       controller.value = controller.value.copyWith(
-        isInitialized: true,
-        isRecordingVideo: true,
-        deviceOrientation: DeviceOrientation.portraitUp,
-        lockedCaptureOrientation:
-            const Optional<DeviceOrientation>.fromNullable(
-                DeviceOrientation.landscapeRight),
-        recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
-            DeviceOrientation.landscapeLeft),
-        previewSize: const Size(480, 640),
-      );
+          isInitialized: true,
+          isRecordingVideo: true,
+          deviceOrientation: DeviceOrientation.portraitDown,
+          lockedCaptureOrientation:
+              const Optional<DeviceOrientation>.fromNullable(
+                  DeviceOrientation.landscapeRight),
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.portraitUp),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
 
       await tester.pumpWidget(
         Directionality(
@@ -170,13 +171,13 @@
 
       final RotatedBox rotatedBox =
           tester.widget<RotatedBox>(find.byType(RotatedBox));
-      expect(rotatedBox.quarterTurns, 3);
+      expect(rotatedBox.quarterTurns, 0);
 
       debugDefaultTargetPlatformOverride = null;
     });
 
     testWidgets(
-        'when orientation locked rotatedBox should turn according to locked orientation',
+        'when recording in DeviceOrientaiton.landscapeRight, rotatedBox should be rotated by one clockwise quarter turn',
         (
       WidgetTester tester,
     ) async {
@@ -184,16 +185,18 @@
 
       final FakeController controller = FakeController();
       addTearDown(controller.dispose);
+
       controller.value = controller.value.copyWith(
-        isInitialized: true,
-        deviceOrientation: DeviceOrientation.portraitUp,
-        lockedCaptureOrientation:
-            const Optional<DeviceOrientation>.fromNullable(
-                DeviceOrientation.landscapeRight),
-        recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
-            DeviceOrientation.landscapeLeft),
-        previewSize: const Size(480, 640),
-      );
+          isInitialized: true,
+          isRecordingVideo: true,
+          deviceOrientation: DeviceOrientation.portraitUp,
+          lockedCaptureOrientation:
+              const Optional<DeviceOrientation>.fromNullable(
+                  DeviceOrientation.landscapeLeft),
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeRight),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
 
       await tester.pumpWidget(
         Directionality(
@@ -211,7 +214,7 @@
     });
 
     testWidgets(
-        'when not locked and not recording rotatedBox should turn according to device orientation',
+        'when recording in DeviceOrientaiton.portraitDown, rotatedBox should be rotated by two clockwise quarter turns',
         (
       WidgetTester tester,
     ) async {
@@ -219,13 +222,91 @@
 
       final FakeController controller = FakeController();
       addTearDown(controller.dispose);
+
       controller.value = controller.value.copyWith(
-        isInitialized: true,
-        deviceOrientation: DeviceOrientation.portraitUp,
-        recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
-            DeviceOrientation.landscapeLeft),
-        previewSize: const Size(480, 640),
+          isInitialized: true,
+          isRecordingVideo: true,
+          deviceOrientation: DeviceOrientation.portraitUp,
+          lockedCaptureOrientation:
+              const Optional<DeviceOrientation>.fromNullable(
+                  DeviceOrientation.landscapeRight),
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.portraitDown),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
       );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 2);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when recording in DeviceOrientaiton.landscapeLeft, rotatedBox should be rotated by three clockwise quarter turns',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          isRecordingVideo: true,
+          deviceOrientation: DeviceOrientation.portraitUp,
+          lockedCaptureOrientation:
+              const Optional<DeviceOrientation>.fromNullable(
+                  DeviceOrientation.landscapeRight),
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeLeft),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 3);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when orientation locked in DeviceOrientaiton.portaitUp, rotatedBox should not be rotated',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          deviceOrientation: DeviceOrientation.portraitDown,
+          lockedCaptureOrientation:
+              const Optional<DeviceOrientation>.fromNullable(
+                  DeviceOrientation.portraitUp),
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeLeft),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
 
       await tester.pumpWidget(
         Directionality(
@@ -241,6 +322,246 @@
 
       debugDefaultTargetPlatformOverride = null;
     });
+
+    testWidgets(
+        'when orientation locked in DeviceOrientaiton.landscapeRight, rotatedBox should be rotated by one clockwise quarter turn',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          deviceOrientation: DeviceOrientation.portraitDown,
+          lockedCaptureOrientation:
+              const Optional<DeviceOrientation>.fromNullable(
+                  DeviceOrientation.landscapeRight),
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeLeft),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 1);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when orientation locked in DeviceOrientaiton.portraitDown, rotatedBox should be rotated by two clockwise quarter turns',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          deviceOrientation: DeviceOrientation.portraitUp,
+          lockedCaptureOrientation:
+              const Optional<DeviceOrientation>.fromNullable(
+                  DeviceOrientation.portraitDown),
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeLeft),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 2);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when orientation locked in DeviceOrientaiton.landscapeRight, rotatedBox should be rotated by three clockwise quarter turns',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          deviceOrientation: DeviceOrientation.portraitUp,
+          lockedCaptureOrientation:
+              const Optional<DeviceOrientation>.fromNullable(
+                  DeviceOrientation.landscapeRight),
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeLeft),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 1);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when orientation not locked, not recording, and device orientation is portrait up, rotatedBox should not be rotated',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          deviceOrientation: DeviceOrientation.portraitUp,
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeLeft),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 0);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when orientation not locked, not recording, and device orientation is landscape right, rotatedBox should be rotated by one clockwise quarter turn',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          deviceOrientation: DeviceOrientation.landscapeRight,
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeLeft),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 1);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when orientation not locked, not recording, and device orientation is portrait down, rotatedBox should be rotated by two clockwise quarter turns',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          deviceOrientation: DeviceOrientation.portraitDown,
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.landscapeLeft),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 2);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
+
+    testWidgets(
+        'when orientation not locked, not recording, and device orientation is landscape left, rotatedBox should be rotated by three clockwise quarter turns',
+        (
+      WidgetTester tester,
+    ) async {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      final FakeController controller = FakeController();
+      addTearDown(controller.dispose);
+
+      controller.value = controller.value.copyWith(
+          isInitialized: true,
+          deviceOrientation: DeviceOrientation.landscapeLeft,
+          recordingOrientation: const Optional<DeviceOrientation>.fromNullable(
+              DeviceOrientation.portraitDown),
+          previewSize: const Size(480, 640) // preview size irrelevant to test
+          );
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: CameraPreview(controller),
+        ),
+      );
+      expect(find.byType(RotatedBox), findsOneWidget);
+
+      final RotatedBox rotatedBox =
+          tester.widget<RotatedBox>(find.byType(RotatedBox));
+      expect(rotatedBox.quarterTurns, 3);
+
+      debugDefaultTargetPlatformOverride = null;
+    });
   }, skip: kIsWeb);
 
   testWidgets('when not on Android there should not be a rotated box',
@@ -249,9 +570,9 @@
     final FakeController controller = FakeController();
     addTearDown(controller.dispose);
     controller.value = controller.value.copyWith(
-      isInitialized: true,
-      previewSize: const Size(480, 640),
-    );
+        isInitialized: true,
+        previewSize: const Size(480, 640) // preview size irrelevant to test
+        );
 
     await tester.pumpWidget(
       Directionality(
diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md
index 7e190cb..0328dff 100644
--- a/packages/camera/camera_android_camerax/CHANGELOG.md
+++ b/packages/camera/camera_android_camerax/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.6.14
+
+* Fixes incorrect camera preview rotation.
+
 ## 0.6.13
 
 * Adds API support query for image streaming.
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
index 7adf025..93f65cb 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java
@@ -1441,9 +1441,6 @@
     @NonNull
     String getTempFilePath(@NonNull String prefix, @NonNull String suffix);
 
-    @NonNull
-    Boolean isPreviewPreTransformed();
-
     /** The codec used by SystemServicesHostApi. */
     static @NonNull MessageCodec<Object> getCodec() {
       return SystemServicesHostApiCodec.INSTANCE;
@@ -1511,29 +1508,6 @@
           channel.setMessageHandler(null);
         }
       }
-      {
-        BasicMessageChannel<Object> channel =
-            new BasicMessageChannel<>(
-                binaryMessenger,
-                "dev.flutter.pigeon.SystemServicesHostApi.isPreviewPreTransformed",
-                getCodec());
-        if (api != null) {
-          channel.setMessageHandler(
-              (message, reply) -> {
-                ArrayList<Object> wrapped = new ArrayList<Object>();
-                try {
-                  Boolean output = api.isPreviewPreTransformed();
-                  wrapped.add(0, output);
-                } catch (Throwable exception) {
-                  ArrayList<Object> wrappedError = wrapError(exception);
-                  wrapped = wrappedError;
-                }
-                reply.reply(wrapped);
-              });
-        } else {
-          channel.setMessageHandler(null);
-        }
-      }
     }
   }
   /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
@@ -1761,6 +1735,9 @@
 
     void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation);
 
+    @NonNull
+    Boolean surfaceProducerHandlesCropAndRotation();
+
     /** The codec used by PreviewHostApi. */
     static @NonNull MessageCodec<Object> getCodec() {
       return PreviewHostApiCodec.INSTANCE;
@@ -1898,6 +1875,29 @@
           channel.setMessageHandler(null);
         }
       }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger,
+                "dev.flutter.pigeon.PreviewHostApi.surfaceProducerHandlesCropAndRotation",
+                getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                try {
+                  Boolean output = api.surfaceProducerHandlesCropAndRotation();
+                  wrapped.add(0, output);
+                } catch (Throwable exception) {
+                  ArrayList<Object> wrappedError = wrapError(exception);
+                  wrapped = wrappedError;
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
     }
   }
   /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java
index 92202dd..ffec50a 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java
@@ -155,7 +155,10 @@
   public void releaseFlutterSurfaceTexture() {
     if (flutterSurfaceProducer != null) {
       flutterSurfaceProducer.release();
+      return;
     }
+    throw new IllegalStateException(
+        "releaseFlutterSurfaceTexture() cannot be called if the flutterSurfaceProducer for the camera preview has not yet been initialized.");
   }
 
   /** Returns the resolution information for the specified {@link Preview}. */
@@ -179,6 +182,16 @@
     preview.setTargetRotation(rotation.intValue());
   }
 
+  @NonNull
+  @Override
+  public Boolean surfaceProducerHandlesCropAndRotation() {
+    if (flutterSurfaceProducer != null) {
+      return flutterSurfaceProducer.handlesCropAndRotation();
+    }
+    throw new IllegalStateException(
+        "surfaceProducerHandlesCropAndRotation() cannot be called if the flutterSurfaceProducer for the camera preview has not yet been initialized.");
+  }
+
   /** Retrieves the {@link Preview} instance associated with the specified {@code identifier}. */
   private Preview getPreviewInstance(@NonNull Long identifier) {
     return Objects.requireNonNull(instanceManager.getInstance(identifier));
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
index 138e925..d058d62 100644
--- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java
@@ -6,7 +6,6 @@
 
 import android.app.Activity;
 import android.content.Context;
-import android.os.Build;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -104,18 +103,4 @@
           null);
     }
   }
-
-  /**
-   * Returns whether or not Impeller uses an {@code ImageReader} backend to provide a {@code
-   * Surface} to CameraX to build the preview. If it is backed by an {@code ImageReader}, then
-   * CameraX will not automatically apply the transformation needed to correct the preview.
-   *
-   * <p>This is determined by the engine, which approximately uses {@code SurfaceTexture}s on
-   * Android SDKs below 29.
-   */
-  @Override
-  @NonNull
-  public Boolean isPreviewPreTransformed() {
-    return Build.VERSION.SDK_INT < 29;
-  }
 }
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java
index 6fd43c2..bc2577b 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java
@@ -263,6 +263,20 @@
     verify(mockPreview).setTargetRotation(targetRotation);
   }
 
+  @Test
+  public void
+      surfaceProducerHandlesCropAndRotation_returnsIfSurfaceProducerHandlesCropAndRotation() {
+    final PreviewHostApiImpl hostApi =
+        new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry);
+    final TextureRegistry.SurfaceProducer mockSurfaceProducer =
+        mock(TextureRegistry.SurfaceProducer.class);
+
+    hostApi.flutterSurfaceProducer = mockSurfaceProducer;
+    when(mockSurfaceProducer.handlesCropAndRotation()).thenReturn(true);
+
+    assertEquals(hostApi.surfaceProducerHandlesCropAndRotation(), true);
+  }
+
   // TODO(bparrishMines): Replace with inline calls to onSurfaceCleanup once available on stable;
   // see https://github.com/flutter/flutter/issues/16125. This separate method only exists to scope
   // the suppression.
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
index 52d02e6..fdfc1b2 100644
--- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java
@@ -5,9 +5,7 @@
 package io.flutter.plugins.camerax;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
@@ -33,7 +31,6 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
 
 @RunWith(RobolectricTestRunner.class)
 public class SystemServicesTest {
@@ -136,28 +133,4 @@
 
     mockedStaticFile.close();
   }
-
-  @Test
-  @Config(sdk = 28)
-  public void isPreviewPreTransformed_returnsTrueWhenRunningBelowSdk29() {
-    final SystemServicesHostApiImpl systemServicesHostApi =
-        new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
-    assertTrue(systemServicesHostApi.isPreviewPreTransformed());
-  }
-
-  @Test
-  @Config(sdk = 28)
-  public void isPreviewPreTransformed_returnsTrueWhenRunningSdk28() {
-    final SystemServicesHostApiImpl systemServicesHostApi =
-        new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
-    assertTrue(systemServicesHostApi.isPreviewPreTransformed());
-  }
-
-  @Test
-  @Config(sdk = 29)
-  public void isPreviewPreTransformed_returnsFalseWhenRunningAboveSdk28() {
-    final SystemServicesHostApiImpl systemServicesHostApi =
-        new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
-    assertFalse(systemServicesHostApi.isPreviewPreTransformed());
-  }
 }
diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart
index 31cde2f..8e61727 100644
--- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart
+++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart
@@ -47,6 +47,7 @@
 import 'resolution_filter.dart';
 import 'resolution_selector.dart';
 import 'resolution_strategy.dart';
+import 'rotated_preview.dart';
 import 'surface.dart';
 import 'system_services.dart';
 import 'use_case.dart';
@@ -238,12 +239,18 @@
   late bool cameraIsFrontFacing;
 
   /// The camera sensor orientation.
+  ///
+  /// This can change if the camera being used changes. Also, it is independent
+  /// of the device orientation or user interface orientation.
   @visibleForTesting
-  late int sensorOrientation;
+  late double sensorOrientationDegrees;
 
-  /// Subscription for listening to changes in device orientation.
-  StreamSubscription<DeviceOrientationChangedEvent>?
-      _subscriptionForDeviceOrientationChanges;
+  /// Whether or not the Android surface producer automatically handles
+  /// correcting the rotation of camera previews for the device this plugin runs on.
+  late bool _handlesCropAndRotation;
+
+  /// The initial orientation of the device when the camera is created.
+  late DeviceOrientation _initialDeviceOrientation;
 
   /// Returns list of all available cameras and their descriptions.
   @override
@@ -380,10 +387,10 @@
 
     // Retrieve info required for correcting the rotation of the camera preview
     // if necessary.
-
-    final Camera2CameraInfo camera2CameraInfo =
-        await proxy.getCamera2CameraInfo(cameraInfo!);
-    sensorOrientation = await proxy.getSensorOrientation(camera2CameraInfo);
+    sensorOrientationDegrees = cameraDescription.sensorOrientation.toDouble();
+    _handlesCropAndRotation =
+        await proxy.previewSurfaceProducerHandlesCropAndRotation(preview!);
+    _initialDeviceOrientation = await proxy.getUiOrientation();
 
     return flutterSurfaceTextureId;
   }
@@ -444,7 +451,6 @@
     await liveCameraState?.removeObservers();
     processCameraProvider?.unbindAll();
     await imageAnalysis?.clearAnalyzer();
-    await _subscriptionForDeviceOrientationChanges?.cancel();
   }
 
   /// The camera has been initialized.
@@ -836,7 +842,30 @@
       );
     }
 
-    return Texture(textureId: cameraId);
+    final Widget preview = Texture(textureId: cameraId);
+
+    if (_handlesCropAndRotation) {
+      return preview;
+    }
+
+    final Stream<DeviceOrientation> deviceOrientationStream =
+        onDeviceOrientationChanged()
+            .map((DeviceOrientationChangedEvent e) => e.orientation);
+    if (cameraIsFrontFacing) {
+      return RotatedPreview.frontFacingCamera(
+        _initialDeviceOrientation,
+        deviceOrientationStream,
+        sensorOrientationDegrees: sensorOrientationDegrees,
+        child: preview,
+      );
+    } else {
+      return RotatedPreview.backFacingCamera(
+        _initialDeviceOrientation,
+        deviceOrientationStream,
+        sensorOrientationDegrees: sensorOrientationDegrees,
+        child: preview,
+      );
+    }
   }
 
   /// Captures an image and returns the file where it was saved.
diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
index a9461ea..e830a28 100644
--- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
+++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart
@@ -1003,33 +1003,6 @@
       return (replyList[0] as String?)!;
     }
   }
-
-  Future<bool> isPreviewPreTransformed() async {
-    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-        'dev.flutter.pigeon.SystemServicesHostApi.isPreviewPreTransformed',
-        codec,
-        binaryMessenger: _binaryMessenger);
-    final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
-    if (replyList == null) {
-      throw PlatformException(
-        code: 'channel-error',
-        message: 'Unable to establish connection on channel.',
-      );
-    } else if (replyList.length > 1) {
-      throw PlatformException(
-        code: replyList[0]! as String,
-        message: replyList[1] as String?,
-        details: replyList[2],
-      );
-    } else if (replyList[0] == null) {
-      throw PlatformException(
-        code: 'null-error',
-        message: 'Host platform returned null value for non-null return value.',
-      );
-    } else {
-      return (replyList[0] as bool?)!;
-    }
-  }
 }
 
 abstract class SystemServicesFlutterApi {
@@ -1356,6 +1329,33 @@
       return;
     }
   }
+
+  Future<bool> surfaceProducerHandlesCropAndRotation() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.PreviewHostApi.surfaceProducerHandlesCropAndRotation',
+        codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyList[0] as bool?)!;
+    }
+  }
 }
 
 class VideoCaptureHostApi {
diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart
index feae868..c5d92d2 100644
--- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart
+++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart
@@ -70,6 +70,8 @@
     this.getCamera2CameraInfo = _getCamera2CameraInfo,
     this.getUiOrientation = _getUiOrientation,
     this.getSensorOrientation = _getSensorOrientation,
+    this.previewSurfaceProducerHandlesCropAndRotation =
+        _previewSurfaceProducerHandlesCropAndRotation,
   });
 
   /// Returns a [ProcessCameraProvider] instance.
@@ -200,6 +202,11 @@
   Future<int> Function(Camera2CameraInfo camera2CameraInfo)
       getSensorOrientation;
 
+  /// Returns whether or not the preview's surface producer handles correctly
+  /// rotating the camera preview automatically.
+  Future<bool> Function(Preview preview)
+      previewSurfaceProducerHandlesCropAndRotation;
+
   static Future<ProcessCameraProvider> _getProcessCameraProvider() {
     return ProcessCameraProvider.getInstance();
   }
@@ -355,4 +362,9 @@
       Camera2CameraInfo camera2CameraInfo) async {
     return camera2CameraInfo.getSensorOrientation();
   }
+
+  static Future<bool> _previewSurfaceProducerHandlesCropAndRotation(
+      Preview preview) async {
+    return preview.surfaceProducerHandlesCropAndRotation();
+  }
 }
diff --git a/packages/camera/camera_android_camerax/lib/src/preview.dart b/packages/camera/camera_android_camerax/lib/src/preview.dart
index 8990313..76da65e 100644
--- a/packages/camera/camera_android_camerax/lib/src/preview.dart
+++ b/packages/camera/camera_android_camerax/lib/src/preview.dart
@@ -86,6 +86,12 @@
   Future<ResolutionInfo> getResolutionInfo() {
     return _api.getResolutionInfoFromInstance(this);
   }
+
+  /// Returns whether or not the Android surface producer automatically handles
+  /// correcting the rotation of camera previews for the device this plugin runs on.
+  Future<bool> surfaceProducerHandlesCropAndRotation() {
+    return _api.surfaceProducerHandlesCropAndRotationFromInstance();
+  }
 }
 
 /// Host API implementation of [Preview].
@@ -156,4 +162,10 @@
 
     return resolutionInfo;
   }
+
+  /// Returns whether or not the Android surface producer automatically handles
+  /// correcting the rotation of camera previews for the device this plugin runs on.
+  Future<bool> surfaceProducerHandlesCropAndRotationFromInstance() {
+    return surfaceProducerHandlesCropAndRotation();
+  }
 }
diff --git a/packages/camera/camera_android_camerax/lib/src/rotated_preview.dart b/packages/camera/camera_android_camerax/lib/src/rotated_preview.dart
new file mode 100644
index 0000000..849de39
--- /dev/null
+++ b/packages/camera/camera_android_camerax/lib/src/rotated_preview.dart
@@ -0,0 +1,119 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:meta/meta.dart';
+
+/// Widget that rotates the camera preview to be upright according to the
+/// current user interface orientation.
+@internal
+final class RotatedPreview extends StatefulWidget {
+  /// Creates [RotatedPreview] that will correct the preview
+  /// rotation assuming that the front camera is being used.
+  const RotatedPreview.frontFacingCamera(
+    this.initialDeviceOrientation,
+    this.deviceOrientation, {
+    required this.sensorOrientationDegrees,
+    required this.child,
+    super.key,
+  }) : facingSign = 1;
+
+  /// Creates [RotatedPreview] that will correct the preview
+  /// rotation assuming that the back camera is being used.
+  const RotatedPreview.backFacingCamera(
+    this.initialDeviceOrientation,
+    this.deviceOrientation, {
+    required this.child,
+    required this.sensorOrientationDegrees,
+    super.key,
+  }) : facingSign = -1;
+
+  /// The initial orientation of the device when the camera is created.
+  final DeviceOrientation initialDeviceOrientation;
+
+  /// The orientation of the device using the camera.
+  final Stream<DeviceOrientation> deviceOrientation;
+
+  /// The orienation of the camera sensor in degrees.
+  final double sensorOrientationDegrees;
+
+  /// The camera preview [Widget] to rotate.
+  final Widget child;
+
+  /// Value used to calculate the correct preview rotation.
+  ///
+  /// 1 if the camera is front facing; -1 if the camera is back facing.
+  final int facingSign;
+
+  @override
+  State<StatefulWidget> createState() => _RotatedPreviewState();
+}
+
+final class _RotatedPreviewState extends State<RotatedPreview> {
+  late DeviceOrientation deviceOrientation;
+  late StreamSubscription<DeviceOrientation> deviceOrientationSubscription;
+
+  @override
+  void initState() {
+    deviceOrientation = widget.initialDeviceOrientation;
+    deviceOrientationSubscription =
+        widget.deviceOrientation.listen((DeviceOrientation event) {
+      // Ensure that we aren't updating the state if the widget is being destroyed.
+      if (!mounted) {
+        return;
+      }
+      setState(() {
+        deviceOrientation = event;
+      });
+    });
+    super.initState();
+  }
+
+  double _computeRotationDegrees(
+    DeviceOrientation orientation, {
+    required double sensorOrientationDegrees,
+    required int sign,
+  }) {
+    final double deviceOrientationDegrees = switch (orientation) {
+      DeviceOrientation.portraitUp => 0,
+      DeviceOrientation.landscapeRight => 90,
+      DeviceOrientation.portraitDown => 180,
+      DeviceOrientation.landscapeLeft => 270,
+    };
+
+    // Rotate the camera preview according to
+    // https://developer.android.com/media/camera/camera2/camera-preview#orientation_calculation.
+    double rotationDegrees =
+        (sensorOrientationDegrees - deviceOrientationDegrees * sign + 360) %
+            360;
+
+    // Then, subtract the rotation already applied in the CameraPreview widget
+    // (see camera/camera/lib/src/camera_preview.dart).
+    rotationDegrees -= deviceOrientationDegrees;
+
+    return rotationDegrees;
+  }
+
+  @override
+  void dispose() {
+    deviceOrientationSubscription.cancel();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final double rotationDegrees = _computeRotationDegrees(
+      deviceOrientation,
+      sensorOrientationDegrees: widget.sensorOrientationDegrees,
+      sign: widget.facingSign,
+    );
+    return RotatedBox(
+      quarterTurns: rotationDegrees ~/ 90,
+      child: widget.child,
+    );
+  }
+}
diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart
index 5f59bd2..b75a1cb 100644
--- a/packages/camera/camera_android_camerax/lib/src/system_services.dart
+++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart
@@ -48,21 +48,6 @@
         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<bool> isPreviewPreTransformed(
-      {BinaryMessenger? binaryMessenger}) {
-    final SystemServicesHostApi api =
-        SystemServicesHostApi(binaryMessenger: binaryMessenger);
-    return api.isPreviewPreTransformed();
-  }
 }
 
 /// Host API implementation of [SystemServices].
diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
index 949830d..a6e4ede 100644
--- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
+++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
@@ -260,8 +260,6 @@
   CameraPermissionsErrorData? requestCameraPermissions(bool enableAudio);
 
   String getTempFilePath(String prefix, String suffix);
-
-  bool isPreviewPreTransformed();
 }
 
 @FlutterApi()
@@ -297,6 +295,8 @@
   ResolutionInfo getResolutionInfo(int identifier);
 
   void setTargetRotation(int identifier, int rotation);
+
+  bool surfaceProducerHandlesCropAndRotation();
 }
 
 @HostApi(dartHostTestHandler: 'TestVideoCaptureHostApi')
diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml
index 2aa8901..d8fa612 100644
--- a/packages/camera/camera_android_camerax/pubspec.yaml
+++ b/packages/camera/camera_android_camerax/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Android implementation of the camera plugin using the CameraX library.
 repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.6.13
+version: 0.6.14
 
 environment:
   sdk: ^3.6.0
diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart
index 89e201b..2e4a5c8 100644
--- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart
+++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart
@@ -50,7 +50,7 @@
 import 'package:camera_platform_interface/camera_platform_interface.dart';
 import 'package:flutter/services.dart'
     show DeviceOrientation, PlatformException, Uint8List;
-import 'package:flutter/widgets.dart' show BuildContext, Size, Texture, Widget;
+import 'package:flutter/widgets.dart' show BuildContext, Size;
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
@@ -201,6 +201,8 @@
             Future<Camera2CameraInfo>.value(MockCamera2CameraInfo()),
         getUiOrientation: () =>
             Future<DeviceOrientation>.value(DeviceOrientation.portraitUp),
+        previewSurfaceProducerHandlesCropAndRotation: (_) =>
+            Future<bool>.value(false),
       );
 
   /// CameraXProxy for testing exposure and focus related controls.
@@ -919,7 +921,9 @@
     expect(camera.recorder!.qualitySelector, isNull);
   });
 
-  test('createCamera sets sensor orientation as expected', () async {
+  test(
+      'createCamera sets sensor orientation, handlesCropAndRotation, initialDeviceOrientation as expected',
+      () async {
     final AndroidCameraCameraX camera = AndroidCameraCameraX();
     const CameraLensDirection testLensDirection = CameraLensDirection.back;
     const int testSensorOrientation = 270;
@@ -929,6 +933,7 @@
         sensorOrientation: testSensorOrientation);
     const bool enableAudio = true;
     const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh;
+    const bool testHandlesCropAndRotation = true;
     const DeviceOrientation testUiOrientation = DeviceOrientation.portraitDown;
 
     // Mock/Detached objects for (typically attached) objects created by
@@ -948,6 +953,8 @@
         getProxyForTestingResolutionPreset(mockProcessCameraProvider);
     camera.proxy.getSensorOrientation =
         (_) async => Future<int>.value(testSensorOrientation);
+    camera.proxy.previewSurfaceProducerHandlesCropAndRotation =
+        (_) async => Future<bool>.value(testHandlesCropAndRotation);
     camera.proxy.getUiOrientation =
         () async => Future<DeviceOrientation>.value(testUiOrientation);
 
@@ -960,7 +967,7 @@
     await camera.createCamera(testCameraDescription, testResolutionPreset,
         enableAudio: enableAudio);
 
-    expect(camera.sensorOrientation, testSensorOrientation);
+    expect(camera.sensorOrientationDegrees, testSensorOrientation);
   });
 
   test(
@@ -1296,6 +1303,8 @@
     expect(camera.cameraControl, equals(mockCameraControl));
   });
 
+  // Further `buildPreview` testing concerning the Widget that it returns is
+  // located in preview_rotation_test.dart.
   test(
       'buildPreview throws an exception if the preview is not bound to the lifecycle',
       () async {
@@ -1310,22 +1319,6 @@
         () => camera.buildPreview(cameraId), throwsA(isA<CameraException>()));
   });
 
-  test(
-      'buildPreview returns a Texture once the preview is bound to the lifecycle if it is backed by a SurfaceTexture',
-      () async {
-    final AndroidCameraCameraX camera = AndroidCameraCameraX();
-    const int cameraId = 37;
-
-    // Tell camera that createCamera has been called and thus, preview has been
-    // bound to the lifecycle of the camera.
-    camera.previewInitiallyBound = true;
-
-    final Widget widget = camera.buildPreview(cameraId);
-
-    expect(widget is Texture, isTrue);
-    expect((widget as Texture).textureId, cameraId);
-  });
-
   group('video recording', () {
     test(
         'startVideoCapturing binds video capture use case, updates saved camera instance and its properties, and starts the recording',
diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart
index 316b127..fc838a4 100644
--- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart
+++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart
@@ -1134,6 +1134,17 @@
           ),
         )),
       ) as _i17.Future<_i10.ResolutionInfo>);
+
+  @override
+  _i17.Future<bool> surfaceProducerHandlesCropAndRotation() =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #surfaceProducerHandlesCropAndRotation,
+          [],
+        ),
+        returnValue: _i17.Future<bool>.value(false),
+        returnValueForMissingStub: _i17.Future<bool>.value(false),
+      ) as _i17.Future<bool>);
 }
 
 /// A class which mocks [ProcessCameraProvider].
@@ -1410,16 +1421,6 @@
           ),
         ),
       ) as String);
-
-  @override
-  bool isPreviewPreTransformed() => (super.noSuchMethod(
-        Invocation.method(
-          #isPreviewPreTransformed,
-          [],
-        ),
-        returnValue: false,
-        returnValueForMissingStub: false,
-      ) as bool);
 }
 
 /// A class which mocks [VideoCapture].
diff --git a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart
new file mode 100644
index 0000000..e06d16c
--- /dev/null
+++ b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart
@@ -0,0 +1,637 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:camera_android_camerax/camera_android_camerax.dart';
+import 'package:camera_android_camerax/src/camera_selector.dart';
+import 'package:camera_android_camerax/src/camerax_library.g.dart';
+import 'package:camera_android_camerax/src/camerax_proxy.dart';
+import 'package:camera_android_camerax/src/device_orientation_manager.dart';
+import 'package:camera_android_camerax/src/fallback_strategy.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+
+import 'android_camera_camerax_test.mocks.dart';
+
+// Constants to map clockwise degree rotations to quarter turns:
+const int _90DegreesClockwise = 1;
+const int _270DegreesClockwise = 3;
+
+void main() {
+  /// Sets up mock CameraSelector and mock ProcessCameraProvider used to
+  /// select test camera when `availableCameras` is called.
+  ///
+  /// Also mocks a call for mock ProcessCameraProvider that is irrelevant
+  /// to this test.
+  ///
+  /// Returns mock ProcessCameraProvider that is used to select test camera.
+  MockProcessCameraProvider
+      setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera(
+          {required MockCameraSelector mockCameraSelector,
+          required int sensorRotationDegrees}) {
+    final MockProcessCameraProvider mockProcessCameraProvider =
+        MockProcessCameraProvider();
+    final MockCameraInfo mockCameraInfo = MockCameraInfo();
+    final MockCamera mockCamera = MockCamera();
+
+    // Mock retrieving available test camera.
+    when(mockProcessCameraProvider.bindToLifecycle(any, any))
+        .thenAnswer((_) async => mockCamera);
+    when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo);
+    when(mockProcessCameraProvider.getAvailableCameraInfos())
+        .thenAnswer((_) async => <MockCameraInfo>[mockCameraInfo]);
+    when(mockCameraSelector.filter(<MockCameraInfo>[mockCameraInfo]))
+        .thenAnswer((_) async => <MockCameraInfo>[mockCameraInfo]);
+    when(mockCameraInfo.getSensorRotationDegrees())
+        .thenAnswer((_) async => sensorRotationDegrees);
+
+    // Mock additional ProcessCameraProvider operation that is irrelevant
+    // for the tests in this file.
+    when(mockCameraInfo.getCameraState())
+        .thenAnswer((_) async => MockLiveCameraState());
+
+    return mockProcessCameraProvider;
+  }
+
+  /// Returns CameraXProxy used to mock all calls to native Android in
+  /// the `availableCameras` and `createCameraWithSettings` methods.
+  CameraXProxy getProxyForCreatingTestCamera(
+          {required MockProcessCameraProvider mockProcessCameraProvider,
+          required CameraSelector Function(int) createCameraSelector,
+          required bool handlesCropAndRotation,
+          required Future<DeviceOrientation> Function() getUiOrientation}) =>
+      CameraXProxy(
+        getProcessCameraProvider: () async => mockProcessCameraProvider,
+        createCameraSelector: createCameraSelector,
+        previewSurfaceProducerHandlesCropAndRotation: (_) =>
+            Future<bool>.value(handlesCropAndRotation),
+        getUiOrientation: getUiOrientation,
+        createPreview: (_, __) => MockPreview(),
+        createImageCapture: (_, __) => MockImageCapture(),
+        createRecorder: (_) => MockRecorder(),
+        createVideoCapture: (_) async => MockVideoCapture(),
+        createImageAnalysis: (_, __) => MockImageAnalysis(),
+        createResolutionStrategy: (
+                {bool highestAvailable = false,
+                Size? boundSize,
+                int? fallbackRule}) =>
+            MockResolutionStrategy(),
+        createResolutionSelector: (_, __, ___) => MockResolutionSelector(),
+        createFallbackStrategy: (
+                {required VideoQuality quality,
+                required VideoResolutionFallbackRule fallbackRule}) =>
+            MockFallbackStrategy(),
+        createQualitySelector: (
+                {required VideoQuality videoQuality,
+                required FallbackStrategy fallbackStrategy}) =>
+            MockQualitySelector(),
+        createCameraStateObserver: (_) => MockObserver(),
+        requestCameraPermissions: (_) => Future<void>.value(),
+        startListeningForDeviceOrientationChange: (_, __) {},
+        setPreviewSurfaceProvider: (_) => Future<int>.value(
+            3), // 3 is a random Flutter SurfaceTexture ID for testing
+        createAspectRatioStrategy: (int aspectRatio, int fallbackRule) =>
+            MockAspectRatioStrategy(),
+        createResolutionFilterWithOnePreferredSize:
+            (Size preferredResolution) => MockResolutionFilter(),
+      );
+
+  /// Returns function that a CameraXProxy can use to select the front camera.
+  MockCameraSelector Function(int cameraSelectorLensDirection)
+      createCameraSelectorForFrontCamera(
+          MockCameraSelector mockCameraSelector) {
+    return (int cameraSelectorLensDirection) {
+      switch (cameraSelectorLensDirection) {
+        case CameraSelector.lensFacingFront:
+          return mockCameraSelector;
+        default:
+          return MockCameraSelector();
+      }
+    };
+  }
+
+  /// Returns function that a CameraXProxy can use to select the back camera.
+  MockCameraSelector Function(int cameraSelectorLensDirection)
+      createCameraSelectorForBackCamera(MockCameraSelector mockCameraSelector) {
+    return (int cameraSelectorLensDirection) {
+      switch (cameraSelectorLensDirection) {
+        case CameraSelector.lensFacingBack:
+          return mockCameraSelector;
+        default:
+          return MockCameraSelector();
+      }
+    };
+  }
+
+  /// Error message for detecting an incorrect preview rotation.
+  String getExpectedRotationTestFailureReason(
+          int expectedQuarterTurns, int actualQuarterTurns) =>
+      'Expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated $actualQuarterTurns quarter turns.';
+
+  testWidgets(
+      'when handlesCropAndRotation is true, the preview is an unrotated Texture',
+      (WidgetTester tester) async {
+    final AndroidCameraCameraX camera = AndroidCameraCameraX();
+    const int cameraId = 537;
+    const MediaSettings testMediaSettings =
+        MediaSettings(); // media settings irrelevant for test
+
+    // Set up test camera (specifics irrelevant for this test) and
+    // tell camera that handlesCropAndRotation is true.
+    final MockCameraSelector mockCameraSelector = MockCameraSelector();
+    final MockProcessCameraProvider mockProcessCameraProvider =
+        setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera(
+            mockCameraSelector: mockCameraSelector,
+            sensorRotationDegrees: /* irrelevant for test */ 90);
+    camera.proxy = getProxyForCreatingTestCamera(
+        mockProcessCameraProvider: mockProcessCameraProvider,
+        createCameraSelector: (_) => mockCameraSelector,
+        handlesCropAndRotation: true,
+        /* irrelevant for test */ getUiOrientation: () =>
+            Future<DeviceOrientation>.value(DeviceOrientation.landscapeLeft));
+
+    // Get and create test camera.
+    final List<CameraDescription> availableCameras =
+        await camera.availableCameras();
+    expect(availableCameras.length, 1);
+    await camera.createCameraWithSettings(
+        availableCameras.first, testMediaSettings);
+
+    // Put camera preview in widget tree.
+    await tester.pumpWidget(camera.buildPreview(cameraId));
+
+    // Verify Texture was built.
+    final Texture texture = tester.widget<Texture>(find.byType(Texture));
+    expect(texture.textureId, cameraId);
+
+    // Verify RotatedBox was not built and thus, the Texture is not rotated.
+    expect(() => tester.widget<RotatedBox>(find.byType(RotatedBox)),
+        throwsStateError);
+  });
+
+  group('when handlesCropAndRotation is false,', () {
+    // Test that preview rotation responds to initial device orientation:
+    group('sensor orientation degrees is 270, camera is front facing,', () {
+      late AndroidCameraCameraX camera;
+      late int cameraId;
+      late MockCameraSelector mockFrontCameraSelector;
+      late MockCameraSelector Function(int cameraSelectorLensDirection)
+          proxyCreateCameraSelectorForFrontCamera;
+      late MockProcessCameraProvider mockProcessCameraProviderForFrontCamera;
+      late MediaSettings testMediaSettings;
+
+      setUp(() {
+        camera = AndroidCameraCameraX();
+        cameraId = 27;
+
+        // Create and set up mock CameraSelector and mock ProcessCameraProvider for test front camera
+        // with sensor orientation degrees 270.
+        mockFrontCameraSelector = MockCameraSelector();
+        proxyCreateCameraSelectorForFrontCamera =
+            createCameraSelectorForFrontCamera(mockFrontCameraSelector);
+        mockProcessCameraProviderForFrontCamera =
+            setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera(
+                mockCameraSelector: mockFrontCameraSelector,
+                sensorRotationDegrees: 270);
+
+        // Media settings to create camera; irrelevant for test.
+        testMediaSettings = const MediaSettings();
+      });
+
+      testWidgets(
+          'initial device orientation fixed to DeviceOrientation.portraitUp, then the preview Texture is rotated 270 degrees clockwise',
+          (WidgetTester tester) async {
+        // Set up test to use front camera, tell camera that handlesCropAndRotation is false,
+        // set camera initial device orientation to portrait up.
+        camera.proxy = getProxyForCreatingTestCamera(
+            mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera,
+            createCameraSelector: proxyCreateCameraSelectorForFrontCamera,
+            handlesCropAndRotation: false,
+            getUiOrientation: () =>
+                Future<DeviceOrientation>.value(DeviceOrientation.portraitUp));
+
+        // Get and create test front camera.
+        final List<CameraDescription> availableCameras =
+            await camera.availableCameras();
+        expect(availableCameras.length, 1);
+        await camera.createCameraWithSettings(
+            availableCameras.first, testMediaSettings);
+
+        // Put camera preview in widget tree.
+        await tester.pumpWidget(camera.buildPreview(cameraId));
+
+        // Verify Texture is rotated by ((270 - 0 * 1 + 360) % 360) - 0 = 270 degrees.
+        const int expectedQuarterTurns = _270DegreesClockwise;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(rotatedBox.quarterTurns, expectedQuarterTurns,
+            reason: getExpectedRotationTestFailureReason(
+                expectedQuarterTurns, rotatedBox.quarterTurns));
+      });
+
+      testWidgets(
+          'initial device orientation fixed to DeviceOrientation.landscapeRight, then the preview Texture is rotated 90 degrees clockwise',
+          (WidgetTester tester) async {
+        // Set up test to use front camera, tell camera that handlesCropAndRotation is false,
+        // set camera initial device orientation to landscape right.
+        camera.proxy = getProxyForCreatingTestCamera(
+            mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera,
+            createCameraSelector: proxyCreateCameraSelectorForFrontCamera,
+            handlesCropAndRotation: false,
+            getUiOrientation: () => Future<DeviceOrientation>.value(
+                DeviceOrientation.landscapeRight));
+
+        // Get and create test front camera.
+        final List<CameraDescription> availableCameras =
+            await camera.availableCameras();
+        expect(availableCameras.length, 1);
+        await camera.createCameraWithSettings(
+            availableCameras.first, testMediaSettings);
+
+        // Put camera preview in widget tree.
+        await tester.pumpWidget(camera.buildPreview(cameraId));
+
+        // Verify Texture is rotated by ((90 - 270 * 1 + 360) % 360) - 90 = 90 degrees.
+        const int expectedQuarterTurns = _90DegreesClockwise;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(rotatedBox.quarterTurns, expectedQuarterTurns,
+            reason: getExpectedRotationTestFailureReason(
+                expectedQuarterTurns, rotatedBox.quarterTurns));
+      });
+
+      testWidgets(
+          'initial device orientation fixed to DeviceOrientation.portraitDown, then the preview Texture is rotated 270 degrees clockwise',
+          (WidgetTester tester) async {
+        // Set up test to use front camera, tell camera that handlesCropAndRotation is false,
+        // set camera initial device orientation to portrait down.
+        camera.proxy = getProxyForCreatingTestCamera(
+            mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera,
+            createCameraSelector: proxyCreateCameraSelectorForFrontCamera,
+            handlesCropAndRotation: false,
+            getUiOrientation: () => Future<DeviceOrientation>.value(
+                DeviceOrientation.portraitDown));
+
+        // Get and create test front camera.
+        final List<CameraDescription> availableCameras =
+            await camera.availableCameras();
+        expect(availableCameras.length, 1);
+        await camera.createCameraWithSettings(
+            availableCameras.first, testMediaSettings);
+
+        // Put camera preview in widget tree.
+        await tester.pumpWidget(camera.buildPreview(cameraId));
+
+        // Verify Texture is rotated by ((270 - 180 * 1 + 360) % 360) - 180 = -90 degrees clockwise = 90 degrees counterclockwise = 270 degrees.
+        const int expectedQuarterTurns = _270DegreesClockwise;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4;
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(clockwiseQuarterTurns, expectedQuarterTurns,
+            reason: getExpectedRotationTestFailureReason(
+                expectedQuarterTurns, rotatedBox.quarterTurns));
+      });
+
+      testWidgets(
+          'initial device orientation fixed to DeviceOrientation.landscapeLeft, then the preview Texture is rotated 90 degrees clockwise',
+          (WidgetTester tester) async {
+        // Set up test to use front camera, tell camera that handlesCropAndRotation is false,
+        // set camera initial device orientation to landscape left.
+        camera.proxy = getProxyForCreatingTestCamera(
+            mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera,
+            createCameraSelector: proxyCreateCameraSelectorForFrontCamera,
+            handlesCropAndRotation: false,
+            getUiOrientation: () => Future<DeviceOrientation>.value(
+                DeviceOrientation.landscapeLeft));
+
+        // Get and create test front camera.
+        final List<CameraDescription> availableCameras =
+            await camera.availableCameras();
+        expect(availableCameras.length, 1);
+        await camera.createCameraWithSettings(
+            availableCameras.first, testMediaSettings);
+
+        // Put camera preview in widget tree.
+        await tester.pumpWidget(camera.buildPreview(cameraId));
+
+        // Verify Texture is rotated by ((270 - 270 * 1 + 360) % 360) - 270 = -270 degrees clockwise = 270 degrees counterclockwise = 90 degrees.
+        const int expectedQuarterTurns = _90DegreesClockwise;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4;
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(clockwiseQuarterTurns, expectedQuarterTurns,
+            reason: getExpectedRotationTestFailureReason(
+                expectedQuarterTurns, rotatedBox.quarterTurns));
+      });
+    });
+
+    testWidgets(
+        'sensor orientation degrees is 90, camera is front facing, then the preview Texture rotates correctly as the device orientation rotates',
+        (WidgetTester tester) async {
+      final AndroidCameraCameraX camera = AndroidCameraCameraX();
+      const int cameraId = 3372;
+
+      // Create and set up mock CameraSelector and mock ProcessCameraProvider for test front camera
+      // with sensor orientation degrees 90.
+      final MockCameraSelector mockFrontCameraSelector = MockCameraSelector();
+      final MockCameraSelector Function(int cameraSelectorLensDirection)
+          proxyCreateCameraSelectorForFrontCamera =
+          createCameraSelectorForFrontCamera(mockFrontCameraSelector);
+      final MockProcessCameraProvider mockProcessCameraProviderForFrontCamera =
+          setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera(
+              mockCameraSelector: mockFrontCameraSelector,
+              sensorRotationDegrees: 90);
+
+      // Media settings to create camera; irrelevant for test.
+      const MediaSettings testMediaSettings = MediaSettings();
+
+      // Set up test to use front camera and tell camera that handlesCropAndRotation is false,
+      // set camera initial device orientation to landscape left.
+      camera.proxy = getProxyForCreatingTestCamera(
+          mockProcessCameraProvider: mockProcessCameraProviderForFrontCamera,
+          createCameraSelector: proxyCreateCameraSelectorForFrontCamera,
+          handlesCropAndRotation: false,
+          getUiOrientation: /* initial device orientation irrelevant for test */
+              () => Future<DeviceOrientation>.value(
+                  DeviceOrientation.landscapeLeft));
+
+      // Get and create test front camera.
+      final List<CameraDescription> availableCameras =
+          await camera.availableCameras();
+      expect(availableCameras.length, 1);
+      await camera.createCameraWithSettings(
+          availableCameras.first, testMediaSettings);
+
+      // Calculated according to:
+      // ((90 - currentDeviceOrientation * 1 + 360) % 360) - currentDeviceOrientation.
+      final Map<DeviceOrientation, int> expectedRotationPerDeviceOrientation =
+          <DeviceOrientation, int>{
+        DeviceOrientation.portraitUp: _90DegreesClockwise,
+        DeviceOrientation.landscapeRight: _270DegreesClockwise,
+        DeviceOrientation.portraitDown: _90DegreesClockwise,
+        DeviceOrientation.landscapeLeft: _270DegreesClockwise,
+      };
+
+      // Put camera preview in widget tree.
+      await tester.pumpWidget(camera.buildPreview(cameraId));
+
+      for (final DeviceOrientation currentDeviceOrientation
+          in expectedRotationPerDeviceOrientation.keys) {
+        final DeviceOrientationChangedEvent testEvent =
+            DeviceOrientationChangedEvent(currentDeviceOrientation);
+        DeviceOrientationManager.deviceOrientationChangedStreamController
+            .add(testEvent);
+
+        await tester.pumpAndSettle();
+
+        // Verify Texture is rotated by expected clockwise degrees.
+        final int expectedQuarterTurns =
+            expectedRotationPerDeviceOrientation[currentDeviceOrientation]!;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        final int clockwiseQuarterTurns = rotatedBox.quarterTurns < 0
+            ? rotatedBox.quarterTurns + 4
+            : rotatedBox.quarterTurns;
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(clockwiseQuarterTurns, expectedQuarterTurns,
+            reason:
+                'When the device orientation is $currentDeviceOrientation, expected the preview to be rotated by $expectedQuarterTurns quarter turns (which is ${expectedQuarterTurns * 90} degrees clockwise) but instead was rotated ${rotatedBox.quarterTurns} quarter turns.');
+      }
+
+      await DeviceOrientationManager.deviceOrientationChangedStreamController
+          .close();
+    });
+
+    // Test the preview rotation responds to the two most common sensor orientations for Android phone cameras; see
+    // https://developer.android.com/media/camera/camera2/camera-preview#camera_orientation.
+    group(
+        'initial device orientation is DeviceOrientation.landscapeLeft, camera is back facing,',
+        () {
+      late AndroidCameraCameraX camera;
+      late int cameraId;
+      late MockCameraSelector mockBackCameraSelector;
+      late MockCameraSelector Function(int cameraSelectorLensDirection)
+          proxyCreateCameraSelectorForBackCamera;
+      late MediaSettings testMediaSettings;
+      late DeviceOrientation testInitialDeviceOrientation;
+
+      setUp(() {
+        camera = AndroidCameraCameraX();
+        cameraId = 347;
+
+        // Set test camera initial device orientation for test.
+        testInitialDeviceOrientation = DeviceOrientation.landscapeLeft;
+
+        // Create and set up mock CameraSelector and mock ProcessCameraProvider for test back camera
+        // with sensor orientation degrees 270.
+        mockBackCameraSelector = MockCameraSelector();
+        proxyCreateCameraSelectorForBackCamera =
+            createCameraSelectorForBackCamera(mockBackCameraSelector);
+
+        testMediaSettings = const MediaSettings();
+      });
+
+      testWidgets(
+          'sensor orientation degrees is 90, then the preview Texture is rotated 90 degrees clockwise',
+          (WidgetTester tester) async {
+        // Create mock ProcessCameraProvider that will acknowledge that the test back camera with sensor orientation degrees
+        // 90 is available.
+        final MockProcessCameraProvider mockProcessCameraProviderForBackCamera =
+            setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera(
+                mockCameraSelector: mockBackCameraSelector,
+                sensorRotationDegrees: 90);
+
+        // Set up test to use back camera, tell camera that handlesCropAndRotation is false,
+        // set camera initial device orientation to landscape left.
+        camera.proxy = getProxyForCreatingTestCamera(
+            mockProcessCameraProvider: mockProcessCameraProviderForBackCamera,
+            createCameraSelector: proxyCreateCameraSelectorForBackCamera,
+            handlesCropAndRotation: false,
+            getUiOrientation: () =>
+                Future<DeviceOrientation>.value(testInitialDeviceOrientation));
+
+        // Get and create test back camera.
+        final List<CameraDescription> availableCameras =
+            await camera.availableCameras();
+        expect(availableCameras.length, 1);
+        await camera.createCameraWithSettings(
+            availableCameras.first, testMediaSettings);
+
+        // Put camera preview in widget tree.
+        await tester.pumpWidget(camera.buildPreview(cameraId));
+
+        // Verify Texture is rotated by ((90 - 270 * -1 + 360) % 360) - 270 = -270 degrees clockwise = 270 degrees counterclockwise = 90 degrees clockwise.
+        const int expectedQuarterTurns = _90DegreesClockwise;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4;
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(clockwiseQuarterTurns, expectedQuarterTurns,
+            reason: getExpectedRotationTestFailureReason(
+                expectedQuarterTurns, rotatedBox.quarterTurns));
+      });
+
+      testWidgets(
+          'sensor orientation degrees is 270, then the preview Texture is rotated 270 degrees clockwise',
+          (WidgetTester tester) async {
+        // Create mock ProcessCameraProvider that will acknowledge that the test back camera with sensor orientation degrees
+        // 270 is available.
+        final MockProcessCameraProvider mockProcessCameraProviderForBackCamera =
+            setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera(
+                mockCameraSelector: mockBackCameraSelector,
+                sensorRotationDegrees: 270);
+
+        // Set up test to use back camera, tell camera that handlesCropAndRotation is false,
+        // set camera initial device orientation to landscape left.
+        camera.proxy = getProxyForCreatingTestCamera(
+            mockProcessCameraProvider: mockProcessCameraProviderForBackCamera,
+            createCameraSelector: proxyCreateCameraSelectorForBackCamera,
+            handlesCropAndRotation: false,
+            getUiOrientation: () =>
+                Future<DeviceOrientation>.value(testInitialDeviceOrientation));
+
+        // Get and create test back camera.
+        final List<CameraDescription> availableCameras =
+            await camera.availableCameras();
+        expect(availableCameras.length, 1);
+        await camera.createCameraWithSettings(
+            availableCameras.first, testMediaSettings);
+
+        // Put camera preview in widget tree.
+        await tester.pumpWidget(camera.buildPreview(cameraId));
+
+        // Verify Texture is rotated by ((270 - 270 * -1 + 360) % 360) - 270 = -90 degrees clockwise = 90 degrees counterclockwise = 270 degrees clockwise.
+        const int expectedQuarterTurns = _270DegreesClockwise;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4;
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(clockwiseQuarterTurns, expectedQuarterTurns,
+            reason: getExpectedRotationTestFailureReason(
+                expectedQuarterTurns, rotatedBox.quarterTurns));
+      });
+    });
+
+    // Test the preview rotation responds to the camera being front or back facing:
+    group(
+        'initial device orientation is DeviceOrientation.landscapeRight, sensor orientation degrees is 90,',
+        () {
+      late AndroidCameraCameraX camera;
+      late int cameraId;
+      late MediaSettings testMediaSettings;
+      late DeviceOrientation testInitialDeviceOrientation;
+      late int testSensorOrientation;
+
+      setUp(() {
+        camera = AndroidCameraCameraX();
+        cameraId = 317;
+
+        // Set test camera initial device orientation and sensor orientation for test.
+        testInitialDeviceOrientation = DeviceOrientation.landscapeRight;
+        testSensorOrientation = 90;
+
+        // Media settings to create camera; irrelevant for test.
+        testMediaSettings = const MediaSettings();
+      });
+
+      testWidgets(
+          'camera is front facing, then the preview Texture is rotated 270 degrees clockwise',
+          (WidgetTester tester) async {
+        // Set up test front camera with sensor orientation degrees 90.
+        final MockCameraSelector mockFrontCameraSelector = MockCameraSelector();
+        final MockProcessCameraProvider mockProcessCameraProvider =
+            setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera(
+                mockCameraSelector: mockFrontCameraSelector,
+                sensorRotationDegrees: testSensorOrientation);
+
+        // Set up front camera selection and initial device orientation as landscape right.
+        final MockCameraSelector Function(int cameraSelectorLensDirection)
+            proxyCreateCameraSelectorForFrontCamera =
+            createCameraSelectorForFrontCamera(mockFrontCameraSelector);
+        camera.proxy = getProxyForCreatingTestCamera(
+            mockProcessCameraProvider: mockProcessCameraProvider,
+            createCameraSelector: proxyCreateCameraSelectorForFrontCamera,
+            handlesCropAndRotation: false,
+            getUiOrientation: () =>
+                Future<DeviceOrientation>.value(testInitialDeviceOrientation));
+
+        // Get and create test camera.
+        final List<CameraDescription> availableCameras =
+            await camera.availableCameras();
+        expect(availableCameras.length, 1);
+        await camera.createCameraWithSettings(
+            availableCameras.first, testMediaSettings);
+
+        // Put camera preview in widget tree.
+        await tester.pumpWidget(camera.buildPreview(cameraId));
+
+        // Verify Texture is rotated by ((90 - 90 * 1 + 360) % 360) - 90 = -90 degrees clockwise = 90 degrees counterclockwise = 270 degrees clockwise.
+        const int expectedQuarterTurns = _270DegreesClockwise;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        final int clockwiseQuarterTurns = rotatedBox.quarterTurns + 4;
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(clockwiseQuarterTurns, expectedQuarterTurns,
+            reason: getExpectedRotationTestFailureReason(
+                expectedQuarterTurns, rotatedBox.quarterTurns));
+      });
+
+      testWidgets(
+          'camera is back facing, then the preview Texture is rotated 90 degrees clockwise',
+          (WidgetTester tester) async {
+        // Set up test front camera with sensor orientation degrees 90.
+        final MockCameraSelector mockBackCameraSelector = MockCameraSelector();
+        final MockProcessCameraProvider mockProcessCameraProvider =
+            setUpMockCameraSelectorAndMockProcessCameraProviderForSelectingTestCamera(
+                mockCameraSelector: mockBackCameraSelector,
+                sensorRotationDegrees: testSensorOrientation);
+
+        // Set up front camera selection and initial device orientation as landscape right.
+        final MockCameraSelector Function(int cameraSelectorLensDirection)
+            proxyCreateCameraSelectorForFrontCamera =
+            createCameraSelectorForBackCamera(mockBackCameraSelector);
+        camera.proxy = getProxyForCreatingTestCamera(
+            mockProcessCameraProvider: mockProcessCameraProvider,
+            createCameraSelector: proxyCreateCameraSelectorForFrontCamera,
+            handlesCropAndRotation: false,
+            getUiOrientation: () =>
+                Future<DeviceOrientation>.value(testInitialDeviceOrientation));
+
+        // Get and create test camera.
+        final List<CameraDescription> availableCameras =
+            await camera.availableCameras();
+        expect(availableCameras.length, 1);
+        await camera.createCameraWithSettings(
+            availableCameras.first, testMediaSettings);
+
+        // Put camera preview in widget tree.
+        await tester.pumpWidget(camera.buildPreview(cameraId));
+
+        // Verify Texture is rotated by ((90 - 90 * -1 + 360) % 360) - 90 = 90 degrees clockwise.
+        const int expectedQuarterTurns = _90DegreesClockwise;
+        final RotatedBox rotatedBox =
+            tester.widget<RotatedBox>(find.byType(RotatedBox));
+        expect(rotatedBox.child, isA<Texture>());
+        expect((rotatedBox.child! as Texture).textureId, cameraId);
+        expect(rotatedBox.quarterTurns, expectedQuarterTurns,
+            reason: getExpectedRotationTestFailureReason(
+                expectedQuarterTurns, rotatedBox.quarterTurns));
+      });
+    });
+  });
+}
diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart
index f9b5da0..4800b04 100644
--- a/packages/camera/camera_android_camerax/test/preview_test.dart
+++ b/packages/camera/camera_android_camerax/test/preview_test.dart
@@ -170,5 +170,19 @@
 
       verify(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview)));
     });
+
+    test(
+        'surfaceProducerHandlesCropAndRotation makes call to check if Android surface producer automatically corrects camera preview rotation',
+        () async {
+      final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi();
+      TestPreviewHostApi.setup(mockApi);
+      final Preview preview = Preview.detached();
+
+      when(mockApi.surfaceProducerHandlesCropAndRotation()).thenReturn(true);
+
+      expect(await preview.surfaceProducerHandlesCropAndRotation(), true);
+
+      verify(mockApi.surfaceProducerHandlesCropAndRotation());
+    });
   });
 }
diff --git a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart
index 8976a31..d8b7a21 100644
--- a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart
+++ b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart
@@ -127,6 +127,15 @@
         ),
         returnValueForMissingStub: null,
       );
+
+  @override
+  bool surfaceProducerHandlesCropAndRotation() => (super.noSuchMethod(
+        Invocation.method(
+          #surfaceProducerHandlesCropAndRotation,
+          [],
+        ),
+        returnValue: false,
+      ) as bool);
 }
 
 /// A class which mocks [ResolutionSelector].
diff --git a/packages/camera/camera_android_camerax/test/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart
index d172551..030f9ae 100644
--- a/packages/camera/camera_android_camerax/test/system_services_test.dart
+++ b/packages/camera/camera_android_camerax/test/system_services_test.dart
@@ -85,16 +85,4 @@
       verify(mockApi.getTempFilePath(testPrefix, testSuffix));
     });
   });
-
-  test('isPreviewPreTransformed returns expected answer', () async {
-    final MockTestSystemServicesHostApi mockApi =
-        MockTestSystemServicesHostApi();
-    TestSystemServicesHostApi.setup(mockApi);
-    const bool isPreviewPreTransformed = true;
-
-    when(mockApi.isPreviewPreTransformed()).thenReturn(isPreviewPreTransformed);
-
-    expect(await SystemServices.isPreviewPreTransformed(), isTrue);
-    verify(mockApi.isPreviewPreTransformed());
-  });
 }
diff --git a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart
index 9abb64c..ec97625 100644
--- a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart
+++ b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart
@@ -87,13 +87,4 @@
           ),
         ),
       ) 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 32235b8..1007d97 100644
--- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart
+++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart
@@ -510,8 +510,6 @@
 
   String getTempFilePath(String prefix, String suffix);
 
-  bool isPreviewPreTransformed();
-
   static void setup(TestSystemServicesHostApi? api,
       {BinaryMessenger? binaryMessenger}) {
     {
@@ -563,24 +561,6 @@
         });
       }
     }
-    {
-      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
-          'dev.flutter.pigeon.SystemServicesHostApi.isPreviewPreTransformed',
-          codec,
-          binaryMessenger: binaryMessenger);
-      if (api == null) {
-        _testBinaryMessengerBinding!.defaultBinaryMessenger
-            .setMockDecodedMessageHandler<Object?>(channel, null);
-      } else {
-        _testBinaryMessengerBinding!.defaultBinaryMessenger
-            .setMockDecodedMessageHandler<Object?>(channel,
-                (Object? message) async {
-          // ignore message
-          final bool output = api.isPreviewPreTransformed();
-          return <Object?>[output];
-        });
-      }
-    }
   }
 }
 
@@ -722,6 +702,8 @@
 
   void setTargetRotation(int identifier, int rotation);
 
+  bool surfaceProducerHandlesCropAndRotation();
+
   static void setup(TestPreviewHostApi? api,
       {BinaryMessenger? binaryMessenger}) {
     {
@@ -835,6 +817,24 @@
         });
       }
     }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.PreviewHostApi.surfaceProducerHandlesCropAndRotation',
+          codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        _testBinaryMessengerBinding!.defaultBinaryMessenger
+            .setMockDecodedMessageHandler<Object?>(channel, null);
+      } else {
+        _testBinaryMessengerBinding!.defaultBinaryMessenger
+            .setMockDecodedMessageHandler<Object?>(channel,
+                (Object? message) async {
+          // ignore message
+          final bool output = api.surfaceProducerHandlesCropAndRotation();
+          return <Object?>[output];
+        });
+      }
+    }
   }
 }