[camera] Fix for CameraAccessException that prevents image capture on certain devices running Android 7/8 (#4572)

diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index 4b96fd6..35a958e 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,7 +1,12 @@
+## 0.9.4+16
+
+* Fixes a bug resulting in a `CameraAccessException` that prevents image
+  capture on some Android devices.
+
 ## 0.9.4+15
 
 * Uses dispatch queue for pixel buffer synchronization on iOS.
-* Minor iOS internal code cleanup related to queue helper functions.  
+* Minor iOS internal code cleanup related to queue helper functions.
 
 ## 0.9.4+14
 
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 6a70ea0..0521c42 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
@@ -79,6 +79,24 @@
   void onError(String errorCode, String errorMessage);
 }
 
+/** A mockable wrapper for CameraDevice calls. */
+interface CameraDeviceWrapper {
+  @NonNull
+  CaptureRequest.Builder createCaptureRequest(int templateType) throws CameraAccessException;
+
+  @TargetApi(VERSION_CODES.P)
+  void createCaptureSession(SessionConfiguration config) throws CameraAccessException;
+
+  @TargetApi(VERSION_CODES.LOLLIPOP)
+  void createCaptureSession(
+      @NonNull List<Surface> outputs,
+      @NonNull CameraCaptureSession.StateCallback callback,
+      @Nullable Handler handler)
+      throws CameraAccessException;
+
+  void close();
+}
+
 class Camera
     implements CameraCaptureCallback.CameraCaptureStateListener,
         ImageReader.OnImageAvailableListener {
@@ -114,7 +132,7 @@
   /** An additional thread for running tasks that shouldn't block the UI. */
   private HandlerThread backgroundHandlerThread;
 
-  private CameraDevice cameraDevice;
+  private CameraDeviceWrapper cameraDevice;
   private CameraCaptureSession captureSession;
   private ImageReader pictureImageReader;
   private ImageReader imageStreamReader;
@@ -136,6 +154,44 @@
 
   private MethodChannel.Result flutterResult;
 
+  /** A CameraDeviceWrapper implementation that forwards calls to a CameraDevice. */
+  private class DefaultCameraDeviceWrapper implements CameraDeviceWrapper {
+    private final CameraDevice cameraDevice;
+
+    private DefaultCameraDeviceWrapper(CameraDevice cameraDevice) {
+      this.cameraDevice = cameraDevice;
+    }
+
+    @NonNull
+    @Override
+    public CaptureRequest.Builder createCaptureRequest(int templateType)
+        throws CameraAccessException {
+      return cameraDevice.createCaptureRequest(templateType);
+    }
+
+    @TargetApi(VERSION_CODES.P)
+    @Override
+    public void createCaptureSession(SessionConfiguration config) throws CameraAccessException {
+      cameraDevice.createCaptureSession(config);
+    }
+
+    @TargetApi(VERSION_CODES.LOLLIPOP)
+    @SuppressWarnings("deprecation")
+    @Override
+    public void createCaptureSession(
+        @NonNull List<Surface> outputs,
+        @NonNull CameraCaptureSession.StateCallback callback,
+        @Nullable Handler handler)
+        throws CameraAccessException {
+      cameraDevice.createCaptureSession(outputs, callback, backgroundHandler);
+    }
+
+    @Override
+    public void close() {
+      cameraDevice.close();
+    }
+  }
+
   public Camera(
       final Activity activity,
       final SurfaceTextureEntry flutterTexture,
@@ -261,7 +317,7 @@
         new CameraDevice.StateCallback() {
           @Override
           public void onOpened(@NonNull CameraDevice device) {
-            cameraDevice = device;
+            cameraDevice = new DefaultCameraDeviceWrapper(device);
             try {
               startPreview();
               dartMessenger.sendCameraInitializedEvent(
@@ -584,7 +640,6 @@
 
     try {
       captureSession.stopRepeating();
-      captureSession.abortCaptures();
       Log.i(TAG, "sending capture request");
       captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler);
     } catch (CameraAccessException e) {
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 1ed2e4c..167733b 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
@@ -22,11 +22,15 @@
 import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
+import android.media.ImageReader;
 import android.media.MediaRecorder;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.view.Surface;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.lifecycle.LifecycleObserver;
 import io.flutter.embedding.engine.systemchannels.PlatformChannel;
 import io.flutter.plugin.common.MethodChannel;
@@ -50,11 +54,39 @@
 import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
 import io.flutter.plugins.camera.utils.TestUtils;
 import io.flutter.view.TextureRegistry;
+import java.util.ArrayList;
+import java.util.List;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.MockedStatic;
 
+class FakeCameraDeviceWrapper implements CameraDeviceWrapper {
+  final List<CaptureRequest.Builder> captureRequests;
+
+  FakeCameraDeviceWrapper(List<CaptureRequest.Builder> captureRequests) {
+    this.captureRequests = captureRequests;
+  }
+
+  @NonNull
+  @Override
+  public CaptureRequest.Builder createCaptureRequest(int var1) {
+    return captureRequests.remove(0);
+  }
+
+  @Override
+  public void createCaptureSession(SessionConfiguration config) {}
+
+  @Override
+  public void createCaptureSession(
+      @NonNull List<Surface> outputs,
+      @NonNull CameraCaptureSession.StateCallback callback,
+      @Nullable Handler handler) {}
+
+  @Override
+  public void close() {}
+}
+
 public class CameraTest {
   private CameraProperties mockCameraProperties;
   private CameraFeatureFactory mockCameraFeatureFactory;
@@ -801,6 +833,29 @@
     verify(mockHandlerThread, times(1)).start();
   }
 
+  @Test
+  public void onConverge_shouldTakePictureWithoutAbortingSession() throws CameraAccessException {
+    ArrayList<CaptureRequest.Builder> mockRequestBuilders = new ArrayList<>();
+    mockRequestBuilders.add(mock(CaptureRequest.Builder.class));
+    CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders);
+    // Stub out other features used by the flow.
+    TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera);
+    TestUtils.setPrivateField(camera, "pictureImageReader", mock(ImageReader.class));
+    SensorOrientationFeature mockSensorOrientationFeature =
+        mockCameraFeatureFactory.createSensorOrientationFeature(mockCameraProperties, null, null);
+    DeviceOrientationManager mockDeviceOrientationManager = mock(DeviceOrientationManager.class);
+    when(mockSensorOrientationFeature.getDeviceOrientationManager())
+        .thenReturn(mockDeviceOrientationManager);
+
+    // Simulate a post-precapture flow.
+    camera.onConverged();
+    // A picture should be taken.
+    verify(mockCaptureSession, times(1)).capture(any(), any(), any());
+    // The session shuold not be aborted as part of this flow, as this breaks capture on some
+    // devices, and causes delays on others.
+    verify(mockCaptureSession, never()).abortCaptures();
+  }
+
   private static class TestCameraFeatureFactory implements CameraFeatureFactory {
     private final AutoFocusFeature mockAutoFocusFeature;
     private final ExposureLockFeature mockExposureLockFeature;
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index fcdce02..2baab09 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
   Dart.
 repository: https://github.com/flutter/plugins/tree/main/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.4+15
+version: 0.9.4+16
 
 environment:
   sdk: ">=2.14.0 <3.0.0"