From 17bbff09f343f679c0c1a592b4df0527d95693c5 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 3 Dec 2021 11:21:29 +0100 Subject: [PATCH 1/7] [camera] Fix for CameraAccessException affecting certain devices on Android 7/8 (#4572) --- packages/camera/camera/CHANGELOG.md | 4 ++++ .../src/main/java/io/flutter/plugins/camera/Camera.java | 8 ++++++-- packages/camera/camera/pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 1a6eceb957b1..0049826bbae1 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.4+6 + +* Fixes bug resulting in a `CameraAccessException` that prevents image capture on certain devices running Android 7/8. + ## 0.9.4+5 * Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception. 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 6a70ea0d10ea..efa19601cd1a 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 @@ -583,8 +583,12 @@ public void onCaptureCompleted( }; try { - captureSession.stopRepeating(); - captureSession.abortCaptures(); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) { + captureSession.stopRepeating(); + } else { + 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/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 58e1ca3ca98c..4c6c7e7e52d1 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing 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.4+5 +version: 0.9.4+6 environment: sdk: ">=2.14.0 <3.0.0" From 3c5ba8f7c94cff5b9ba30203884a84e36f946575 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 31 Dec 2021 14:10:35 +0100 Subject: [PATCH 2/7] Moved stopRepeating outside of if/else and added notes --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 efa19601cd1a..8ec50ebeb1f7 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 @@ -583,10 +583,9 @@ public void onCaptureCompleted( }; try { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) { - captureSession.stopRepeating(); - } else { - captureSession.stopRepeating(); + captureSession.stopRepeating(); + /** Conditional statement below is fix for CameraAccessException that prevents image capture on certain devices running Android 7/8 */ + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { captureSession.abortCaptures(); } Log.i(TAG, "sending capture request"); From a64449ffdc7729a91632ea85816cf056d1b750a7 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 31 Dec 2021 14:20:12 +0100 Subject: [PATCH 3/7] Amended note --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ec50ebeb1f7..36191e708d05 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 @@ -584,7 +584,7 @@ public void onCaptureCompleted( try { captureSession.stopRepeating(); - /** Conditional statement below is fix for CameraAccessException that prevents image capture on certain devices running Android 7/8 */ + // Conditional statement below is fix for CameraAccessException that prevents image capture on certain devices running Android 7/8 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { captureSession.abortCaptures(); } From cfac8244d685af9add5b09480a8f220a9e899778 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 14 Jan 2022 13:15:23 +0100 Subject: [PATCH 4/7] Removed captureSession.abortCaptures from takePictureAfterPrecapture --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 4 ---- 1 file changed, 4 deletions(-) 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 36191e708d05..342c9905e9a9 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 @@ -584,10 +584,6 @@ public void onCaptureCompleted( try { captureSession.stopRepeating(); - // Conditional statement below is fix for CameraAccessException that prevents image capture on certain devices running Android 7/8 - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { - captureSession.abortCaptures(); - } Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler); } catch (CameraAccessException e) { From e07ca93a2c55047fd6bb932f378e565fdc312eec Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 11 Feb 2022 14:11:03 -0500 Subject: [PATCH 5/7] Add unit test --- .../io/flutter/plugins/camera/Camera.java | 60 ++++++++++++++++++- .../io/flutter/plugins/camera/CameraTest.java | 55 +++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) 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 342c9905e9a9..d124dc6b9bdb 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 @@ interface ErrorCallback { void onError(String errorCode, String errorMessage); } +/** A mockable wrapper for CameraDevice calls. */ +interface CameraDeviceWrapper { + @NonNull + CaptureRequest.Builder createCaptureRequest(int var1) throws CameraAccessException; + + @TargetApi(VERSION_CODES.P) + void createCaptureSession(SessionConfiguration config) throws CameraAccessException; + + @TargetApi(VERSION_CODES.LOLLIPOP) + void createCaptureSession( + @NonNull List outputs, + @NonNull CameraCaptureSession.StateCallback callback, + @Nullable Handler handler) + throws CameraAccessException; + + void close(); +} + class Camera implements CameraCaptureCallback.CameraCaptureStateListener, ImageReader.OnImageAvailableListener { @@ -114,7 +132,7 @@ class Camera /** 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 @@ class Camera 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 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 @@ public void open(String imageFormatGroup) throws CameraAccessException { new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { - cameraDevice = device; + cameraDevice = new DefaultCameraDeviceWrapper(device); try { startPreview(); dartMessenger.sendCameraInitializedEvent( 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 1ed2e4c11d7b..167733b9dcca 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 captureRequests; + + FakeCameraDeviceWrapper(List 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 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 @@ public void startBackgroundThread_shouldNotStartNewThreadWhenAlreadyCreated() { verify(mockHandlerThread, times(1)).start(); } + @Test + public void onConverge_shouldTakePictureWithoutAbortingSession() throws CameraAccessException { + ArrayList 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; From e9cc6401f31941b64cc10d9cf60a58c130de3b91 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 24 Feb 2022 20:23:02 -0500 Subject: [PATCH 6/7] Variable name fix --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d124dc6b9bdb..0521c422d794 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 @@ -82,7 +82,7 @@ interface ErrorCallback { /** A mockable wrapper for CameraDevice calls. */ interface CameraDeviceWrapper { @NonNull - CaptureRequest.Builder createCaptureRequest(int var1) throws CameraAccessException; + CaptureRequest.Builder createCaptureRequest(int templateType) throws CameraAccessException; @TargetApi(VERSION_CODES.P) void createCaptureSession(SessionConfiguration config) throws CameraAccessException; From 4cf95374798259be7a93685e7aab1e1820ea4e3e Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 8 Mar 2022 11:38:27 -0800 Subject: [PATCH 7/7] Update version again --- packages/camera/camera/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index fcdce02e947f..2baab09c5dcb 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing 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"