[camera] Add Android & iOS implementations for pausing the camera preview  (#4258)

diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index 6d38fa2..bb00480 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.2
+
+* Added functions to pause and resume the camera preview.
+
 ## 0.9.1+1
 
 * Replace `device_info` reference with `device_info_plus` in the [README.md](README.md)
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
index 43479ac..c036c1c 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
@@ -126,6 +126,8 @@
   private MediaRecorder mediaRecorder;
   /** True when recording video. */
   private boolean recordingVideo;
+  /** True when the preview is paused. */
+  private boolean pausedPreview;
 
   private File captureFile;
 
@@ -428,8 +430,10 @@
     }
 
     try {
-      captureSession.setRepeatingRequest(
-          previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
+      if (!pausedPreview) {
+        captureSession.setRepeatingRequest(
+            previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
+      }
 
       if (onSuccessCallback != null) {
         onSuccessCallback.run();
@@ -834,33 +838,36 @@
      * For focus mode an extra step of actually locking/unlocking the
      * focus has to be done, in order to ensure it goes into the correct state.
      */
-    switch (newMode) {
-      case locked:
-        // Perform a single focus trigger.
-        lockAutoFocus();
-        if (captureSession == null) {
-          Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
-          return;
-        }
-
-        // Set AF state to idle again.
-        previewRequestBuilder.set(
-            CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
-
-        try {
-          captureSession.setRepeatingRequest(
-              previewRequestBuilder.build(), null, backgroundHandler);
-        } catch (CameraAccessException e) {
-          if (result != null) {
-            result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null);
+    if (!pausedPreview) {
+      switch (newMode) {
+        case locked:
+          // Perform a single focus trigger.
+          if (captureSession == null) {
+            Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
+            return;
           }
-          return;
-        }
-        break;
-      case auto:
-        // Cancel current AF trigger and set AF to idle again.
-        unlockAutoFocus();
-        break;
+          lockAutoFocus();
+
+          // Set AF state to idle again.
+          previewRequestBuilder.set(
+              CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
+
+          try {
+            captureSession.setRepeatingRequest(
+                previewRequestBuilder.build(), null, backgroundHandler);
+          } catch (CameraAccessException e) {
+            if (result != null) {
+              result.error(
+                  "setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null);
+            }
+            return;
+          }
+          break;
+        case auto:
+          // Cancel current AF trigger and set AF to idle again.
+          unlockAutoFocus();
+          break;
+      }
     }
 
     if (result != null) {
@@ -966,6 +973,19 @@
     cameraFeatures.getSensorOrientation().unlockCaptureOrientation();
   }
 
+  /** Pause the preview from dart. */
+  public void pausePreview() throws CameraAccessException {
+    this.pausedPreview = true;
+    this.captureSession.stopRepeating();
+  }
+
+  /** Resume the preview from dart. */
+  public void resumePreview() {
+    this.pausedPreview = false;
+    this.refreshPreviewCaptureSession(
+        null, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
+  }
+
   public void startPreview() throws CameraAccessException {
     if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
     Log.i(TAG, "startPreview");
@@ -1022,8 +1042,8 @@
   private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) {
     imageStreamReader.setOnImageAvailableListener(
         reader -> {
-          // Use acquireNextImage since image reader is only for one image.
           Image img = reader.acquireNextImage();
+          // Use acquireNextImage since image reader is only for one image.
           if (img == null) return;
 
           List<Map<String, Object>> planes = new ArrayList<>();
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
index 893785f..5e25353 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
@@ -339,6 +339,22 @@
           }
           break;
         }
+      case "pausePreview":
+        {
+          try {
+            camera.pausePreview();
+            result.success(null);
+          } catch (Exception e) {
+            handleException(e, result);
+          }
+          break;
+        }
+      case "resumePreview":
+        {
+          camera.resumePreview();
+          result.success(null);
+          break;
+        }
       case "dispose":
         {
           if (camera != null) {
diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
index cab2ae8..5431df0 100644
--- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
+++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
@@ -744,6 +744,33 @@
     verify(mockSensorOrientationFeature, times(1)).unlockCaptureOrientation();
   }
 
+  @Test
+  public void pausePreview_shouldPausePreview() throws CameraAccessException {
+    camera.pausePreview();
+
+    assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), true);
+    verify(mockCaptureSession, times(1)).stopRepeating();
+  }
+
+  @Test
+  public void resumePreview_shouldResumePreview() throws CameraAccessException {
+    camera.resumePreview();
+
+    assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), false);
+    verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any());
+  }
+
+  @Test
+  public void resumePreview_shouldSendErrorEventOnCameraAccessException()
+      throws CameraAccessException {
+    when(mockCaptureSession.setRepeatingRequest(any(), any(), any()))
+        .thenThrow(new CameraAccessException(0));
+
+    camera.resumePreview();
+
+    verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any());
+  }
+
   private static class TestCameraFeatureFactory implements CameraFeatureFactory {
     private final AutoFocusFeature mockAutoFocusFeature;
     private final ExposureLockFeature mockExposureLockFeature;
diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java
new file mode 100644
index 0000000..35eed7a
--- /dev/null
+++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java
@@ -0,0 +1,69 @@
+// 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.
+
+package io.flutter.plugins.camera;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.hardware.camera2.CameraAccessException;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugins.camera.utils.TestUtils;
+import io.flutter.view.TextureRegistry;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MethodCallHandlerImplTest {
+
+  MethodChannel.MethodCallHandler handler;
+  MethodChannel.Result mockResult;
+  Camera mockCamera;
+
+  @Before
+  public void setUp() {
+    handler =
+        new MethodCallHandlerImpl(
+            mock(Activity.class),
+            mock(BinaryMessenger.class),
+            mock(CameraPermissions.class),
+            mock(CameraPermissions.PermissionsRegistry.class),
+            mock(TextureRegistry.class),
+            null);
+    mockResult = mock(MethodChannel.Result.class);
+    mockCamera = mock(Camera.class);
+    TestUtils.setPrivateField(handler, "camera", mockCamera);
+  }
+
+  @Test
+  public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult()
+      throws CameraAccessException {
+    handler.onMethodCall(new MethodCall("pausePreview", null), mockResult);
+
+    verify(mockCamera, times(1)).pausePreview();
+    verify(mockResult, times(1)).success(null);
+  }
+
+  @Test
+  public void onMethodCall_pausePreview_shouldSendErrorResultOnCameraAccessException()
+      throws CameraAccessException {
+    doThrow(new CameraAccessException(0)).when(mockCamera).pausePreview();
+
+    handler.onMethodCall(new MethodCall("pausePreview", null), mockResult);
+
+    verify(mockResult, times(1)).error("CameraAccess", null, null);
+  }
+
+  @Test
+  public void onMethodCall_resumePreview_shouldResumePreviewAndSendSuccessResult() {
+    handler.onMethodCall(new MethodCall("resumePreview", null), mockResult);
+
+    verify(mockCamera, times(1)).resumePreview();
+    verify(mockResult, times(1)).success(null);
+  }
+}
diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java
index dbf9d11..fce99b5 100644
--- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java
+++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java
@@ -33,4 +33,15 @@
       Assert.fail("Unable to mock private field: " + fieldName);
     }
   }
+
+  public static <T> Object getPrivateField(T instance, String fieldName) {
+    try {
+      Field field = instance.getClass().getDeclaredField(fieldName);
+      field.setAccessible(true);
+      return field.get(instance);
+    } catch (Exception e) {
+      Assert.fail("Unable to mock private field: " + fieldName);
+      return null;
+    }
+  }
 }
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
index aead167..5a622f1 100644
--- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
@@ -18,6 +18,7 @@
 		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
 		A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; };
 		D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; };
+		E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -68,6 +69,7 @@
 		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
 		D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
+		E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -96,6 +98,7 @@
 				03BB766A2665316900CE5A93 /* CameraFocusTests.m */,
 				03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */,
 				03BB766C2665316900CE5A93 /* Info.plist */,
+				E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
 			);
 			path = RunnerTests;
 			sourceTree = "<group>";
@@ -359,6 +362,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */,
+				E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */,
 				334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m
new file mode 100644
index 0000000..549b40a
--- /dev/null
+++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m
@@ -0,0 +1,50 @@
+// 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 camera;
+@import XCTest;
+@import AVFoundation;
+#import <OCMock/OCMock.h>
+
+@interface FLTCam : NSObject <FlutterTexture,
+                              AVCaptureVideoDataOutputSampleBufferDelegate,
+                              AVCaptureAudioDataOutputSampleBufferDelegate>
+@property(assign, nonatomic) BOOL isPreviewPaused;
+- (void)pausePreviewWithResult:(FlutterResult)result;
+- (void)resumePreviewWithResult:(FlutterResult)result;
+@end
+
+@interface CameraPreviewPauseTests : XCTestCase
+@property(readonly, nonatomic) FLTCam* camera;
+@end
+
+@implementation CameraPreviewPauseTests
+
+- (void)setUp {
+  _camera = [[FLTCam alloc] init];
+}
+
+- (void)testPausePreviewWithResult_shouldPausePreview {
+  XCTestExpectation* resultExpectation =
+      [self expectationWithDescription:@"Succeeding result with nil value"];
+  [_camera pausePreviewWithResult:^void(id _Nullable result) {
+    XCTAssertNil(result);
+    [resultExpectation fulfill];
+  }];
+  [self waitForExpectationsWithTimeout:2.0 handler:nil];
+  XCTAssertTrue(_camera.isPreviewPaused);
+}
+
+- (void)testResumePreviewWithResult_shouldResumePreview {
+  XCTestExpectation* resultExpectation =
+      [self expectationWithDescription:@"Succeeding result with nil value"];
+  [_camera resumePreviewWithResult:^void(id _Nullable result) {
+    XCTAssertNil(result);
+    [resultExpectation fulfill];
+  }];
+  [self waitForExpectationsWithTimeout:2.0 handler:nil];
+  XCTAssertFalse(_camera.isPreviewPaused);
+}
+
+@end
diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart
index 2314aec..364f59d 100644
--- a/packages/camera/camera/example/lib/main.dart
+++ b/packages/camera/camera/example/lib/main.dart
@@ -530,7 +530,16 @@
                   cameraController.value.isRecordingVideo
               ? onStopButtonPressed
               : null,
-        )
+        ),
+        IconButton(
+          icon: const Icon(Icons.pause_presentation),
+          color:
+              cameraController != null && cameraController.value.isPreviewPaused
+                  ? Colors.red
+                  : Colors.blue,
+          onPressed:
+              cameraController == null ? null : onPausePreviewButtonPressed,
+        ),
       ],
     );
   }
@@ -747,6 +756,23 @@
     });
   }
 
+  Future<void> onPausePreviewButtonPressed() async {
+    final CameraController? cameraController = controller;
+
+    if (cameraController == null || !cameraController.value.isInitialized) {
+      showInSnackBar('Error: select a camera first.');
+      return;
+    }
+
+    if (cameraController.value.isPreviewPaused) {
+      await cameraController.resumePreview();
+    } else {
+      await cameraController.pausePreview();
+    }
+
+    if (mounted) setState(() {});
+  }
+
   void onPauseButtonPressed() {
     pauseVideoRecording().then((_) {
       if (mounted) setState(() {});
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m
index ea03ce5..cb93e9f 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -330,6 +330,7 @@
 @property(assign, nonatomic) BOOL audioIsDisconnected;
 @property(assign, nonatomic) BOOL isAudioSetup;
 @property(assign, nonatomic) BOOL isStreamingImages;
+@property(assign, nonatomic) BOOL isPreviewPaused;
 @property(assign, nonatomic) ResolutionPreset resolutionPreset;
 @property(assign, nonatomic) ExposureMode exposureMode;
 @property(assign, nonatomic) FocusMode focusMode;
@@ -1035,6 +1036,16 @@
   [captureDevice unlockForConfiguration];
 }
 
+- (void)pausePreviewWithResult:(FlutterResult)result {
+  _isPreviewPaused = true;
+  result(nil);
+}
+
+- (void)resumePreviewWithResult:(FlutterResult)result {
+  _isPreviewPaused = false;
+  result(nil);
+}
+
 - (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation
                                             x:(double)x
                                             y:(double)y {
@@ -1432,7 +1443,9 @@
 
       __weak CameraPlugin *weakSelf = self;
       _camera.onFrameAvailable = ^{
-        [weakSelf.registry textureFrameAvailable:cameraId];
+        if (![weakSelf.camera isPreviewPaused]) {
+          [weakSelf.registry textureFrameAvailable:cameraId];
+        }
       };
       FlutterMethodChannel *methodChannel = [FlutterMethodChannel
           methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu",
@@ -1519,6 +1532,10 @@
         y = ((NSNumber *)call.arguments[@"y"]).doubleValue;
       }
       [_camera setFocusPointWithResult:result x:x y:y];
+    } else if ([@"pausePreview" isEqualToString:call.method]) {
+      [_camera pausePreviewWithResult:result];
+    } else if ([@"resumePreview" isEqualToString:call.method]) {
+      [_camera resumePreviewWithResult:result];
     } else {
       result(FlutterMethodNotImplemented);
     }
diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart
index 37869fe..58193bd 100644
--- a/packages/camera/camera/lib/src/camera_controller.dart
+++ b/packages/camera/camera/lib/src/camera_controller.dart
@@ -47,6 +47,8 @@
     required this.deviceOrientation,
     this.lockedCaptureOrientation,
     this.recordingOrientation,
+    this.isPreviewPaused = false,
+    this.previewPauseOrientation,
   }) : _isRecordingPaused = isRecordingPaused;
 
   /// Creates a new camera controller state for an uninitialized controller.
@@ -63,6 +65,7 @@
           focusMode: FocusMode.auto,
           focusPointSupported: false,
           deviceOrientation: DeviceOrientation.portraitUp,
+          isPreviewPaused: false,
         );
 
   /// True after [CameraController.initialize] has completed successfully.
@@ -79,6 +82,12 @@
 
   final bool _isRecordingPaused;
 
+  /// True when the preview widget has been paused manually.
+  final bool isPreviewPaused;
+
+  /// Set to the orientation the preview was paused in, if it is currently paused.
+  final DeviceOrientation? previewPauseOrientation;
+
   /// True when camera [isRecordingVideo] and recording is paused.
   bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused;
 
@@ -150,6 +159,8 @@
     DeviceOrientation? deviceOrientation,
     Optional<DeviceOrientation>? lockedCaptureOrientation,
     Optional<DeviceOrientation>? recordingOrientation,
+    bool? isPreviewPaused,
+    Optional<DeviceOrientation>? previewPauseOrientation,
   }) {
     return CameraValue(
       isInitialized: isInitialized ?? this.isInitialized,
@@ -172,6 +183,10 @@
       recordingOrientation: recordingOrientation == null
           ? this.recordingOrientation
           : recordingOrientation.orNull,
+      isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused,
+      previewPauseOrientation: previewPauseOrientation == null
+          ? this.previewPauseOrientation
+          : previewPauseOrientation.orNull,
     );
   }
 
@@ -190,7 +205,9 @@
         'focusPointSupported: $focusPointSupported, '
         'deviceOrientation: $deviceOrientation, '
         'lockedCaptureOrientation: $lockedCaptureOrientation, '
-        'recordingOrientation: $recordingOrientation)';
+        'recordingOrientation: $recordingOrientation, '
+        'isPreviewPaused: $isPreviewPaused, '
+        'previewPausedOrientation: $previewPauseOrientation)';
   }
 }
 
@@ -325,6 +342,35 @@
     await CameraPlatform.instance.prepareForVideoRecording();
   }
 
+  /// Pauses the current camera preview
+  Future<void> pausePreview() async {
+    if (value.isPreviewPaused) {
+      return;
+    }
+    try {
+      await CameraPlatform.instance.pausePreview(_cameraId);
+      value = value.copyWith(
+          isPreviewPaused: true,
+          previewPauseOrientation: Optional.of(this.value.deviceOrientation));
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+  }
+
+  /// Resumes the current camera preview
+  Future<void> resumePreview() async {
+    if (!value.isPreviewPaused) {
+      return;
+    }
+    try {
+      await CameraPlatform.instance.resumePreview(_cameraId);
+      value = value.copyWith(
+          isPreviewPaused: false, previewPauseOrientation: Optional.absent());
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+  }
+
   /// Captures an image and returns the file where it was saved.
   ///
   /// Throws a [CameraException] if the capture fails.
diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart
index 1df9f8e..6a15896 100644
--- a/packages/camera/camera/lib/src/camera_preview.dart
+++ b/packages/camera/camera/lib/src/camera_preview.dart
@@ -71,7 +71,8 @@
   DeviceOrientation _getApplicableOrientation() {
     return controller.value.isRecordingVideo
         ? controller.value.recordingOrientation!
-        : (controller.value.lockedCaptureOrientation ??
+        : (controller.value.previewPauseOrientation ??
+            controller.value.lockedCaptureOrientation ??
             controller.value.deviceOrientation);
   }
 }
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index 1009191..3e3fad1 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
   and streaming image buffers to dart.
 repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.1+1
+version: 0.9.2
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -20,7 +20,7 @@
         pluginClass: CameraPlugin
 
 dependencies:
-  camera_platform_interface: ^2.0.0
+  camera_platform_interface: ^2.1.0
   flutter:
     sdk: flutter
   pedantic: ^1.10.0
diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart
index 8275461..14afdda 100644
--- a/packages/camera/camera/test/camera_preview_test.dart
+++ b/packages/camera/camera/test/camera_preview_test.dart
@@ -113,6 +113,12 @@
 
   @override
   Future<void> unlockCaptureOrientation() async {}
+
+  @override
+  Future<void> pausePreview() async {}
+
+  @override
+  Future<void> resumePreview() async {}
 }
 
 void main() {
diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart
index 26382a9..6904e68 100644
--- a/packages/camera/camera/test/camera_test.dart
+++ b/packages/camera/camera/test/camera_test.dart
@@ -1137,6 +1137,138 @@
           .called(4);
     });
 
+    test('pausePreview() calls $CameraPlatform', () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+      cameraController.value = cameraController.value
+          .copyWith(deviceOrientation: DeviceOrientation.portraitUp);
+
+      await cameraController.pausePreview();
+
+      verify(CameraPlatform.instance.pausePreview(cameraController.cameraId))
+          .called(1);
+      expect(cameraController.value.isPreviewPaused, equals(true));
+      expect(cameraController.value.previewPauseOrientation,
+          DeviceOrientation.portraitUp);
+    });
+
+    test('pausePreview() does not call $CameraPlatform when already paused',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+      cameraController.value =
+          cameraController.value.copyWith(isPreviewPaused: true);
+
+      await cameraController.pausePreview();
+
+      verifyNever(
+          CameraPlatform.instance.pausePreview(cameraController.cameraId));
+      expect(cameraController.value.isPreviewPaused, equals(true));
+    });
+
+    test('pausePreview() throws $CameraException on $PlatformException',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+      when(CameraPlatform.instance.pausePreview(cameraController.cameraId))
+          .thenThrow(
+        PlatformException(
+          code: 'TEST_ERROR',
+          message: 'This is a test error message',
+          details: null,
+        ),
+      );
+
+      expect(
+          cameraController.pausePreview(),
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'TEST_ERROR',
+            'This is a test error message',
+          )));
+    });
+
+    test('resumePreview() calls $CameraPlatform', () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+      cameraController.value =
+          cameraController.value.copyWith(isPreviewPaused: true);
+
+      await cameraController.resumePreview();
+
+      verify(CameraPlatform.instance.resumePreview(cameraController.cameraId))
+          .called(1);
+      expect(cameraController.value.isPreviewPaused, equals(false));
+    });
+
+    test('resumePreview() does not call $CameraPlatform when not paused',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+      cameraController.value =
+          cameraController.value.copyWith(isPreviewPaused: false);
+
+      await cameraController.resumePreview();
+
+      verifyNever(
+          CameraPlatform.instance.resumePreview(cameraController.cameraId));
+      expect(cameraController.value.isPreviewPaused, equals(false));
+    });
+
+    test('resumePreview() throws $CameraException on $PlatformException',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+      cameraController.value =
+          cameraController.value.copyWith(isPreviewPaused: true);
+      when(CameraPlatform.instance.resumePreview(cameraController.cameraId))
+          .thenThrow(
+        PlatformException(
+          code: 'TEST_ERROR',
+          message: 'This is a test error message',
+          details: null,
+        ),
+      );
+
+      expect(
+          cameraController.resumePreview(),
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'TEST_ERROR',
+            'This is a test error message',
+          )));
+    });
+
     test('lockCaptureOrientation() calls $CameraPlatform', () async {
       CameraController cameraController = CameraController(
           CameraDescription(
@@ -1315,6 +1447,14 @@
       .noSuchMethod(Invocation.method(#unlockCaptureOrientation, [cameraId]));
 
   @override
+  Future<void> pausePreview(int? cameraId) async =>
+      super.noSuchMethod(Invocation.method(#pausePreview, [cameraId]));
+
+  @override
+  Future<void> resumePreview(int? cameraId) async =>
+      super.noSuchMethod(Invocation.method(#resumePreview, [cameraId]));
+
+  @override
   Future<double> getMaxZoomLevel(int? cameraId) async => super.noSuchMethod(
         Invocation.method(#getMaxZoomLevel, [cameraId]),
         returnValue: 1.0,
diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart
index e0378cc..4718d89 100644
--- a/packages/camera/camera/test/camera_value_test.dart
+++ b/packages/camera/camera/test/camera_value_test.dart
@@ -29,6 +29,8 @@
         lockedCaptureOrientation: DeviceOrientation.portraitUp,
         recordingOrientation: DeviceOrientation.portraitUp,
         focusPointSupported: true,
+        isPreviewPaused: false,
+        previewPauseOrientation: DeviceOrientation.portraitUp,
       );
 
       expect(cameraValue, isA<CameraValue>());
@@ -46,6 +48,8 @@
       expect(
           cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp);
       expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp);
+      expect(cameraValue.isPreviewPaused, false);
+      expect(cameraValue.previewPauseOrientation, DeviceOrientation.portraitUp);
     });
 
     test('Can be created as uninitialized', () {
@@ -66,6 +70,8 @@
       expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp);
       expect(cameraValue.lockedCaptureOrientation, null);
       expect(cameraValue.recordingOrientation, null);
+      expect(cameraValue.isPreviewPaused, isFalse);
+      expect(cameraValue.previewPauseOrientation, null);
     });
 
     test('Can be copied with isInitialized', () {
@@ -87,6 +93,8 @@
       expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp);
       expect(cameraValue.lockedCaptureOrientation, null);
       expect(cameraValue.recordingOrientation, null);
+      expect(cameraValue.isPreviewPaused, isFalse);
+      expect(cameraValue.previewPauseOrientation, null);
     });
 
     test('Has aspectRatio after setting size', () {
@@ -117,25 +125,26 @@
 
     test('toString() works as expected', () {
       var cameraValue = const CameraValue(
-        isInitialized: false,
-        errorDescription: null,
-        previewSize: Size(10, 10),
-        isRecordingPaused: false,
-        isRecordingVideo: false,
-        isTakingPicture: false,
-        isStreamingImages: false,
-        flashMode: FlashMode.auto,
-        exposureMode: ExposureMode.auto,
-        focusMode: FocusMode.auto,
-        exposurePointSupported: true,
-        focusPointSupported: true,
-        deviceOrientation: DeviceOrientation.portraitUp,
-        lockedCaptureOrientation: DeviceOrientation.portraitUp,
-        recordingOrientation: DeviceOrientation.portraitUp,
-      );
+          isInitialized: false,
+          errorDescription: null,
+          previewSize: Size(10, 10),
+          isRecordingPaused: false,
+          isRecordingVideo: false,
+          isTakingPicture: false,
+          isStreamingImages: false,
+          flashMode: FlashMode.auto,
+          exposureMode: ExposureMode.auto,
+          focusMode: FocusMode.auto,
+          exposurePointSupported: true,
+          focusPointSupported: true,
+          deviceOrientation: DeviceOrientation.portraitUp,
+          lockedCaptureOrientation: DeviceOrientation.portraitUp,
+          recordingOrientation: DeviceOrientation.portraitUp,
+          isPreviewPaused: true,
+          previewPauseOrientation: DeviceOrientation.portraitUp);
 
       expect(cameraValue.toString(),
-          'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)');
+          'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp)');
     });
   });
 }