[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"