[camera] Partially Address CameraAccessException: CAMERA_ERROR (#5723)

diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md
index e9972ae..1bc9be5 100644
--- a/packages/camera/camera_android/CHANGELOG.md
+++ b/packages/camera/camera_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.8+3
+
+* Skips duplicate calls to stop background thread and removes unnecessary closings of camera capture sessions on Android.
+
 ## 0.9.8+2
 
 * Fixes exception in registerWith caused by the switch to an in-package method channel.
diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java
index 0521c42..401963c 100644
--- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java
+++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java
@@ -131,6 +131,8 @@
 
   /** An additional thread for running tasks that shouldn't block the UI. */
   private HandlerThread backgroundHandlerThread;
+  /** True when backgroundHandlerThread is in the process of being stopped. */
+  private boolean stoppingBackgroundHandlerThread = false;
 
   private CameraDeviceWrapper cameraDevice;
   private CameraCaptureSession captureSession;
@@ -382,8 +384,8 @@
         backgroundHandler);
   }
 
-  private void createCaptureSession(int templateType, Surface... surfaces)
-      throws CameraAccessException {
+  @VisibleForTesting
+  void createCaptureSession(int templateType, Surface... surfaces) throws CameraAccessException {
     createCaptureSession(templateType, null, surfaces);
   }
 
@@ -391,7 +393,7 @@
       int templateType, Runnable onSuccessCallback, Surface... surfaces)
       throws CameraAccessException {
     // Close any existing capture session.
-    closeCaptureSession();
+    captureSession = null;
 
     // Create a new capture builder.
     previewRequestBuilder = cameraDevice.createCaptureRequest(templateType);
@@ -669,7 +671,11 @@
 
   /** Stops the background thread and its {@link Handler}. */
   public void stopBackgroundThread() {
+    if (stoppingBackgroundHandlerThread) {
+      return;
+    }
     if (backgroundHandlerThread != null) {
+      stoppingBackgroundHandlerThread = true;
       backgroundHandlerThread.quitSafely();
       try {
         backgroundHandlerThread.join();
@@ -679,6 +685,7 @@
     }
     backgroundHandlerThread = null;
     backgroundHandler = null;
+    stoppingBackgroundHandlerThread = false;
   }
 
   /** Start capturing a picture, doing autofocus first. */
@@ -1173,12 +1180,19 @@
 
   public void close() {
     Log.i(TAG, "close");
-    closeCaptureSession();
 
     if (cameraDevice != null) {
       cameraDevice.close();
       cameraDevice = null;
+
+      // Closing the CameraDevice without closing the CameraCaptureSession is recommended
+      // for quickly closing the camera:
+      // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close()
+      captureSession = null;
+    } else {
+      closeCaptureSession();
     }
+
     if (pictureImageReader != null) {
       pictureImageReader.close();
       pictureImageReader = null;
diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
index 167733b..b85b685 100644
--- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
+++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java
@@ -18,8 +18,10 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
+import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.params.SessionConfiguration;
@@ -28,6 +30,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.util.Size;
 import android.view.Surface;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -35,6 +38,7 @@
 import io.flutter.embedding.engine.systemchannels.PlatformChannel;
 import io.flutter.plugin.common.MethodChannel;
 import io.flutter.plugins.camera.features.CameraFeatureFactory;
+import io.flutter.plugins.camera.features.CameraFeatures;
 import io.flutter.plugins.camera.features.Point;
 import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature;
 import io.flutter.plugins.camera.features.autofocus.FocusMode;
@@ -834,6 +838,28 @@
   }
 
   @Test
+  public void stopBackgroundThread_cancelsDuplicateCalls() throws InterruptedException {
+    TestUtils.setPrivateField(camera, "stoppingBackgroundHandlerThread", true);
+
+    camera.startBackgroundThread();
+    camera.stopBackgroundThread();
+
+    verify(mockHandlerThread, never()).quitSafely();
+    verify(mockHandlerThread, never()).join();
+  }
+
+  @Test
+  public void stopBackgroundThread_proceedsWithoutDuplicateCall() throws InterruptedException {
+    TestUtils.setPrivateField(camera, "stoppingBackgroundHandlerThread", false);
+
+    camera.startBackgroundThread();
+    camera.stopBackgroundThread();
+
+    verify(mockHandlerThread).quitSafely();
+    verify(mockHandlerThread).join();
+  }
+
+  @Test
   public void onConverge_shouldTakePictureWithoutAbortingSession() throws CameraAccessException {
     ArrayList<CaptureRequest.Builder> mockRequestBuilders = new ArrayList<>();
     mockRequestBuilders.add(mock(CaptureRequest.Builder.class));
@@ -856,6 +882,52 @@
     verify(mockCaptureSession, never()).abortCaptures();
   }
 
+  @Test
+  public void createCaptureSession_doesNotCloseCaptureSession() throws CameraAccessException {
+    Surface mockSurface = mock(Surface.class);
+    SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class);
+    ResolutionFeature mockResolutionFeature = mock(ResolutionFeature.class);
+    Size mockSize = mock(Size.class);
+    ArrayList<CaptureRequest.Builder> mockRequestBuilders = new ArrayList<>();
+    mockRequestBuilders.add(mock(CaptureRequest.Builder.class));
+    CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders);
+    TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera);
+
+    TextureRegistry.SurfaceTextureEntry cameraFlutterTexture =
+        (TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture");
+    CameraFeatures cameraFeatures =
+        (CameraFeatures) TestUtils.getPrivateField(camera, "cameraFeatures");
+    ResolutionFeature resolutionFeature =
+        (ResolutionFeature)
+            TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature");
+
+    when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture);
+    when(resolutionFeature.getPreviewSize()).thenReturn(mockSize);
+
+    camera.createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, mockSurface);
+
+    verify(mockCaptureSession, never()).close();
+  }
+
+  @Test
+  public void close_doesCloseCaptureSessionWhenCameraDeviceNull() {
+    camera.close();
+
+    verify(mockCaptureSession).close();
+  }
+
+  @Test
+  public void close_doesNotCloseCaptureSessionWhenCameraDeviceNonNull() {
+    ArrayList<CaptureRequest.Builder> mockRequestBuilders = new ArrayList<>();
+    mockRequestBuilders.add(mock(CaptureRequest.Builder.class));
+    CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders);
+    TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera);
+
+    camera.close();
+
+    verify(mockCaptureSession, never()).close();
+  }
+
   private static class TestCameraFeatureFactory implements CameraFeatureFactory {
     private final AutoFocusFeature mockAutoFocusFeature;
     private final ExposureLockFeature mockExposureLockFeature;
diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml
index 7d93ecb..a1b7a93 100644
--- a/packages/camera/camera_android/pubspec.yaml
+++ b/packages/camera/camera_android/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Android implementation of the camera plugin.
 repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.8+2
+version: 0.9.8+3
 
 environment:
   sdk: ">=2.14.0 <3.0.0"