Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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<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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -584,7 +640,6 @@ public void onCaptureCompleted(

try {
captureSession.stopRepeating();
captureSession.abortCaptures();
Log.i(TAG, "sending capture request");
captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler);
} catch (CameraAccessException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -801,6 +833,29 @@ public void startBackgroundThread_shouldNotStartNewThreadWhenAlreadyCreated() {
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;
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down